diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings
index fa933ce4d..309dfd96e 100644
--- a/EFCore.PG.sln.DotSettings
+++ b/EFCore.PG.sln.DotSettings
@@ -190,4 +190,5 @@
True
True
True
+ True
\ No newline at end of file
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/Query/Translations/EnumTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs
index bec6dfe48..dc1432593 100644
--- a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs
@@ -38,7 +38,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
""");
@@ -56,7 +56,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
""");
@@ -77,7 +77,7 @@ await AssertQuery(
"""
@sad='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
""");
@@ -98,7 +98,7 @@ await AssertQuery(
"""
@sad='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
""");
@@ -119,7 +119,7 @@ await AssertQuery(
"""
@sad='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
""");
@@ -140,7 +140,7 @@ await AssertQuery(
"""
@sad='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
""");
@@ -159,7 +159,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%'
""");
@@ -180,7 +180,7 @@ await AssertQuery(
"""
@values='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)
""");
@@ -201,12 +201,33 @@ await AssertQuery(
"""
@values='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)
""");
}
+ [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={ '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)
+""");
+ }
+
#endregion
#region Support
@@ -239,6 +260,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; }
@@ -269,6 +291,12 @@ public enum SchemaQualifiedEnum
Sad
}
+ public enum UppercaseNamedEnum
+ {
+ Happy,
+ Sad
+ }
+
public enum ByteEnum : byte
{
Happy,
@@ -303,7 +331,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.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)