diff --git a/Directory.Build.props b/Directory.Build.props index 712d9ae07..6c16b9a8b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 9.0.2 + 9.0.3 latest true latest diff --git a/Directory.Packages.props b/Directory.Packages.props index 362c7099e..1b0afc7ba 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - [9.0.0,10.0.0) - 9.0.0 + [9.0.1,10.0.0) + 9.0.1 9.0.2 diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 9055198e9..ff5652c30 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -189,4 +189,5 @@ True True True + True \ No newline at end of file diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 800941791..f93fd5c59 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -1266,18 +1266,34 @@ protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, + "https://www.postgresql.org/docs/current/sql-altertype.html)"); } - for (var (newPos, oldPos) = (0, 0); newPos < newLabels.Count; newPos++) + for (var newPos = 0; newPos < newLabels.Count; newPos++) { var newLabel = newLabels[newPos]; - var oldLabel = oldPos < oldLabels.Count ? oldLabels[oldPos] : null; - - if (newLabel == oldLabel) + if (oldLabels.Contains(newLabel)) { - oldPos++; continue; } - GenerateAddEnumLabel(newEnum, newLabel, oldLabel, model, builder); + // We add the new label just after the last one we have in the new labels definition (when the last one is new, it will have + // just been added). + // If the new label happens to be the first one, add it before the first old label. Otherwise, if there are no old labels, + // just append the label (no before/after). + if (newPos == newLabels.Count - 1) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } + else if (newPos > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: newLabels[newPos - 1], model, builder); + } + else if (oldLabels.Count > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: oldLabels[0], afterLabel: null, model, builder); + } + else + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } } } } @@ -1328,9 +1344,15 @@ protected virtual void GenerateAddEnumLabel( PostgresEnum enumType, string addedLabel, string? beforeLabel, + string? afterLabel, IModel? model, MigrationCommandListBuilder builder) { + if (beforeLabel is not null && afterLabel is not null) + { + throw new UnreachableException("Both beforeLabel and afterLabel can't be specified"); + } + var schema = enumType.Schema ?? model?.GetDefaultSchema(); builder @@ -1345,6 +1367,12 @@ protected virtual void GenerateAddEnumLabel( .Append(" BEFORE ") .Append(_stringTypeMapping.GenerateSqlLiteral(beforeLabel)); } + else if (afterLabel is not null) + { + builder + .Append(" AFTER ") + .Append(_stringTypeMapping.GenerateSqlLiteral(afterLabel)); + } builder.AppendLine(";"); diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 6dbbb7e40..e5719e76e 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; @@ -771,6 +772,8 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) { var storeType = mappingInfo.StoreTypeName; var clrType = mappingInfo.ClrType; + string? schema; + string name; if (clrType is not null and not { IsEnum: true, IsClass: false }) { @@ -783,20 +786,31 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) if (storeType is null) { enumDefinition = _enumDefinitions.SingleOrDefault(m => m.ClrType == clrType); + + if (enumDefinition is null) + { + return null; + } + + (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); } else { - // TODO: Not sure what to do about quoting. Is the user expected to configure properties - // TODO: with a quoted (schema-qualified) store type or not? - var dot = storeType.IndexOf('.'); - enumDefinition = dot is -1 - ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType) - : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType[(dot + 1)..] && m.StoreTypeSchema == storeType[..dot]); - } - - if (enumDefinition is null) - { - return null; + // If the user is specifying the store type manually, they are not expected to have quotes in the name (e.g. because of upper- + // case characters). + // However, if we infer an enum array type mapping from an element (e.g. someEnums.Contains(b.SomeEnumColumn)), we get the + // element's store type - which for enums is quoted - and add []; so we get e.g. "MyEnum"[]. So we need to support quoted + // names here, by parsing the name and stripping the quotes. + ParseStoreTypeName(storeType, out name, out schema, out var size, out var precision, out var scale); + + enumDefinition = schema is null + ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name) + : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name && m.StoreTypeSchema == schema); + + if (enumDefinition is null) + { + return null; + } } // We now have an enum definition from the context options. @@ -805,7 +819,6 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) // 1. The quoted type name is used in migrations, where quoting is needed // 2. The unquoted type name is set on NpgsqlParameter.DataTypeName // (though see https://github.com/npgsql/npgsql/issues/5710). - var (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); return new NpgsqlEnumTypeMapping( _sqlGenerationHelper.DelimitIdentifier(name, schema), schema is null ? name : schema + "." + name, @@ -972,6 +985,8 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) ref int? precision, ref int? scale) { + // TODO: Reimplement over ParseStoreTypeName below + if (storeTypeName is null) { return null; @@ -1056,4 +1071,155 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) return new StringBuilder(preParens.Length).Append(preParens).Append(postParens).ToString(); } + + internal static void ParseStoreTypeName( + string storeTypeName, + out string name, + out string? schema, + out int? size, + out int? precision, + out int? scale) + { + var s = storeTypeName.AsSpan().Trim(); + var i = 0; + size = precision = scale = null; + + if (s.EndsWith("[]", StringComparison.Ordinal)) + { + // If this is an array store type, any facets (size, precision...) apply to the element and not to the array (e.g. varchar(32)[] + // is an array mapping with Size=null over an element mapping of varchar with Size=32). So just add everything up to the end. + // Note that if there's a schema (e.g. foo.varchar(32)[]), we return name=varchar(32), schema=foo. + name = s.ToString(); + schema = null; + return; + } + + name = ParseNameComponent(s); + + if (i < s.Length && s[i] == '.') + { + i++; + schema = name; + name = ParseNameComponent(s); + } + else + { + schema = null; + } + + s = s[i..]; + + if (s.Length == 0 || s[0] != '(') + { + // No facets + return; + } + + s = s[1..]; + + var closeParen = s.IndexOf(")", StringComparison.Ordinal); + if (closeParen == -1) + { + return; + } + + var inParens = s[..closeParen].Trim(); + // There may be stuff after the closing parentheses (e.g. timestamp(3) with time zone) + var postParens = s.Slice(closeParen + 1); + + switch (s.IndexOf(",", StringComparison.Ordinal)) + { + // No comma inside the parentheses, parse the value either as size or precision + case -1: + if (!int.TryParse(inParens, out var p)) + { + return; + } + + if (NameBasesUsesPrecision(name)) + { + precision = p; + // scale = 0; + } + else + { + size = p; + } + + break; + + case var comma: + if (int.TryParse(s[..comma].Trim(), out var parsedPrecision)) + { + precision = parsedPrecision; + } + else + { + return; + } + + if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale)) + { + scale = parsedScale; + } + else + { + return; + } + + break; + } + + if (postParens.Length > 0) + { + // There's stuff after the parentheses (e.g. time(3) with time zone), append to the name + name += postParens.ToString(); + } + + string ParseNameComponent(ReadOnlySpan s) + { + var inQuotes = false; + StringBuilder builder = new(); + + if (s[i] == '"') + { + inQuotes = true; + i++; + } + + var start = i; + + for (; i < s.Length; i++) + { + var c = s[i]; + + if (inQuotes) + { + if (c == '"') + { + if (i + 1 < s.Length && s[i + 1] == '"') + { + builder.Append('"'); + i++; + continue; + } + + i++; + break; + } + } + else if (!char.IsWhiteSpace(c) && !char.IsAsciiLetterOrDigit(c) && c != '_') + { + break; + } + + builder.Append(c); + } + + var length = i - start; + return length == storeTypeName.Length + ? storeTypeName + : builder.ToString(); + } + } } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs index a67c241fa..542d3ddda 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs @@ -7,16 +7,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations public class MigrationsInfrastructureNpgsqlTest(MigrationsInfrastructureNpgsqlTest.MigrationsInfrastructureNpgsqlFixture fixture) : MigrationsInfrastructureTestBase(fixture) { - // TODO: Remove once we sync to https://github.com/dotnet/efcore/pull/35106 - public override void Can_generate_no_migration_script() - { - } - - // TODO: Remove once we sync to https://github.com/dotnet/efcore/pull/35106 - public override void Can_generate_migration_from_initial_database_to_initial() - { - } - public override void Can_get_active_provider() { base.Can_get_active_provider(); @@ -24,6 +14,21 @@ public override void Can_get_active_provider() Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", ActiveProvider); } + // See #3407 + public override void Can_apply_two_migrations_in_transaction() + => Assert.ThrowsAny(() => base.Can_apply_two_migrations_in_transaction()); + + // See #3407 + public override Task Can_apply_two_migrations_in_transaction_async() + => Assert.ThrowsAnyAsync(() => base.Can_apply_two_migrations_in_transaction_async()); + + // This tests uses Fixture.CreateEmptyContext(), which does not go through MigrationsInfrastructureNpgsqlFixture.CreateContext() + // and therefore does not set the PostgresVersion in the context options. As a result, we try to drop the database with + // WITH (FORCE), which is only supported starting with PG 13. + [MinimumPostgresVersion(13, 0)] + public override Task Can_generate_no_migration_script() + => base.Can_generate_no_migration_script(); + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] public override void Can_apply_all_migrations() => base.Can_apply_all_migrations(); diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index db6cfe3fd..a641e9f0c 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -2895,7 +2895,25 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' BEFORE 'Sad';"""); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' AFTER 'Happy';"""); + } + + [Fact] + public virtual async Task Alter_enum_change_label_ordering_does_nothing() + { + await Test( + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Sad", "Happy"]), + model => + { + var moodEnum = Assert.Single(model.GetPostgresEnums()); + Assert.Collection( + moodEnum.Labels, + l => Assert.Equal("Happy", l), + l => Assert.Equal("Sad", l)); + }); + + AssertSql(); } [Fact] diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index a3b899507..7467b28f6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -39,7 +39,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = 'sad'::test.mapped_enum """); @@ -57,7 +57,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."SchemaQualifiedEnum" = 'Happy (PgName)'::test.schema_qualified_enum """); @@ -78,7 +78,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -99,7 +99,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -120,7 +120,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -141,7 +141,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -160,7 +160,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum"::text LIKE '%sa%' """); @@ -181,7 +181,7 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."ByteEnum" = ANY (@__values_0) """); @@ -202,12 +202,33 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedByteEnum" = ANY (@__values_0) """); } + [ConditionalTheory] // #3433 + [MemberData(nameof(IsAsyncData))] + public async Task Where_uppercase_enum_array_contains_enum(bool async) + { + await using var ctx = CreateContext(); + + List values = [UppercaseNamedEnum.Sad]; + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.UppercaseNamedEnum))); + + AssertSql( + """ +@__values_0={ 'Sad' } (DbType = Object) + +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" +FROM test."SomeEntities" AS s +WHERE s."UppercaseNamedEnum" = ANY (@__values_0) +"""); + } + #endregion #region Support @@ -240,6 +261,7 @@ public class SomeEnumEntity public UnmappedEnum UnmappedEnum { get; set; } public InferredEnum InferredEnum { get; set; } public SchemaQualifiedEnum SchemaQualifiedEnum { get; set; } + public UppercaseNamedEnum UppercaseNamedEnum { get; set; } public ByteEnum ByteEnum { get; set; } public UnmappedByteEnum UnmappedByteEnum { get; set; } public int EnumValue { get; set; } @@ -270,6 +292,12 @@ public enum SchemaQualifiedEnum Sad } + public enum UppercaseNamedEnum + { + Happy, + Sad + } + public enum ByteEnum : byte { Happy, @@ -304,7 +332,8 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build .MapEnum("mapped_enum", "test") .MapEnum("inferred_enum", "test") .MapEnum("byte_enum", "test") - .MapEnum("schema_qualified_enum", "test"); + .MapEnum("schema_qualified_enum", "test") + .MapEnum("UpperCaseEnum", "test"); return optionsBuilder; } @@ -341,6 +370,7 @@ public IReadOnlyDictionary EntityAsserters Assert.Equal(ee.UnmappedEnum, aa.UnmappedEnum); Assert.Equal(ee.InferredEnum, aa.InferredEnum); Assert.Equal(ee.SchemaQualifiedEnum, aa.SchemaQualifiedEnum); + Assert.Equal(ee.UppercaseNamedEnum, aa.UppercaseNamedEnum); Assert.Equal(ee.ByteEnum, aa.ByteEnum); Assert.Equal(ee.UnmappedByteEnum, aa.UnmappedByteEnum); Assert.Equal(ee.EnumValue, aa.EnumValue); @@ -370,6 +400,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Happy, InferredEnum = InferredEnum.Happy, SchemaQualifiedEnum = SchemaQualifiedEnum.Happy, + UppercaseNamedEnum = UppercaseNamedEnum.Happy, ByteEnum = ByteEnum.Happy, UnmappedByteEnum = UnmappedByteEnum.Happy, EnumValue = (int)MappedEnum.Happy @@ -381,6 +412,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Sad, InferredEnum = InferredEnum.Sad, SchemaQualifiedEnum = SchemaQualifiedEnum.Sad, + UppercaseNamedEnum = UppercaseNamedEnum.Sad, ByteEnum = ByteEnum.Sad, UnmappedByteEnum = UnmappedByteEnum.Sad, EnumValue = (int)MappedEnum.Sad diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index 1fd148a09..91f9d4e29 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -528,6 +528,28 @@ await AssertQuery( """); } + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0={ '10', '999' } (Nullable = false) (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 p."Int" = ANY (@__ints_0) +""", + // + """ +@__ints_0={ '10', '999' } (Nullable = false) (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 NOT (p."Int" = ANY (@__ints_0) AND p."Int" = ANY (@__ints_0) IS NOT NULL) +"""); + } + public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) { await base.Parameter_collection_of_ints_Contains_nullable_int(async); diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs index 3809227de..be5062dda 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs @@ -291,6 +291,35 @@ public void Multirange_by_store_type_across_pg_versions() Assert.Same(typeof(List>), mappingDefault.ClrType); } +#nullable enable + [Theory] + [InlineData("integer", "integer", null, null, null, null)] + [InlineData("integer[]", "integer[]", null, null, null, null)] + [InlineData("foo.bar", "bar", "foo", null, null, null)] + [InlineData("foo.bar[]", "foo.bar[]", null, null, null, null)] + [InlineData("\"foo\"", "foo", null, null, null, null)] + [InlineData("\"fo.o\"", "fo.o", null, null, null, null)] + [InlineData("\"foo\".\"bar\"", "bar", "foo", null, null, null)] + [InlineData("\"f\"\"oo\"", "f\"oo", null, null, null, null)] + [InlineData("character varying", "character varying", null, null, null, null)] + [InlineData("with_underscore", "with_underscore", null, null, null, null)] + [InlineData("varchar(30)", "varchar", null, 30, null, null)] + [InlineData("varchar(30)[]", "varchar(30)[]", null, null, null, null)] + [InlineData("numeric(30)", "numeric", null, null, 30, null)] + [InlineData("numeric(30,3)", "numeric", null, null, 30, 3)] + public void ParseStoreType(string storeTypeName, string expectedName, string? expectedSchema, int? expectedSize, int? expectedPrecision, int? expectedScale) + { + NpgsqlTypeMappingSource.ParseStoreTypeName( + storeTypeName, out var name, out var schema, out var size, out var precision, out var scale); + + Assert.Equal(expectedName, name); + Assert.Equal(expectedSchema, schema); + Assert.Equal(expectedSize, size); + Assert.Equal(expectedPrecision, precision); + Assert.Equal(expectedScale, scale); + } +#nullable restore + #region Support private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion = null)