Skip to content

Commit 2e59bf5

Browse files
committed
Fix quoted enum handling (#3434)
Fixes #3433 (cherry picked from commit 068a7c6)
1 parent c240ce5 commit 2e59bf5

File tree

4 files changed

+250
-22
lines changed

4 files changed

+250
-22
lines changed

EFCore.PG.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,5 @@
189189
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
190190
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EXml_002ECodeStyle_002EFormatSettingsUpgrade_002EXmlMoveToCommonFormatterSettingsUpgrade/@EntryIndexedValue">True</s:Boolean>
191191
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
192+
<s:Boolean x:Key="/Default/UserDictionary/Words/=timestamptz/@EntryIndexedValue">True</s:Boolean>
192193
</wpf:ResourceDictionary>

src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs

Lines changed: 178 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Immutable;
44
using System.Data;
5+
using System.Data.Common;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Net;
78
using System.Net.NetworkInformation;
@@ -771,6 +772,8 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType)
771772
{
772773
var storeType = mappingInfo.StoreTypeName;
773774
var clrType = mappingInfo.ClrType;
775+
string? schema;
776+
string name;
774777

775778
if (clrType is not null and not { IsEnum: true, IsClass: false })
776779
{
@@ -783,20 +786,31 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType)
783786
if (storeType is null)
784787
{
785788
enumDefinition = _enumDefinitions.SingleOrDefault(m => m.ClrType == clrType);
789+
790+
if (enumDefinition is null)
791+
{
792+
return null;
793+
}
794+
795+
(name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema);
786796
}
787797
else
788798
{
789-
// TODO: Not sure what to do about quoting. Is the user expected to configure properties
790-
// TODO: with a quoted (schema-qualified) store type or not?
791-
var dot = storeType.IndexOf('.');
792-
enumDefinition = dot is -1
793-
? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType)
794-
: _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType[(dot + 1)..] && m.StoreTypeSchema == storeType[..dot]);
795-
}
796-
797-
if (enumDefinition is null)
798-
{
799-
return null;
799+
// If the user is specifying the store type manually, they are not expected to have quotes in the name (e.g. because of upper-
800+
// case characters).
801+
// However, if we infer an enum array type mapping from an element (e.g. someEnums.Contains(b.SomeEnumColumn)), we get the
802+
// element's store type - which for enums is quoted - and add []; so we get e.g. "MyEnum"[]. So we need to support quoted
803+
// names here, by parsing the name and stripping the quotes.
804+
ParseStoreTypeName(storeType, out name, out schema, out var size, out var precision, out var scale);
805+
806+
enumDefinition = schema is null
807+
? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name)
808+
: _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name && m.StoreTypeSchema == schema);
809+
810+
if (enumDefinition is null)
811+
{
812+
return null;
813+
}
800814
}
801815

802816
// We now have an enum definition from the context options.
@@ -805,7 +819,6 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType)
805819
// 1. The quoted type name is used in migrations, where quoting is needed
806820
// 2. The unquoted type name is set on NpgsqlParameter.DataTypeName
807821
// (though see https://github.com/npgsql/npgsql/issues/5710).
808-
var (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema);
809822
return new NpgsqlEnumTypeMapping(
810823
_sqlGenerationHelper.DelimitIdentifier(name, schema),
811824
schema is null ? name : schema + "." + name,
@@ -972,6 +985,8 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan<char> span)
972985
ref int? precision,
973986
ref int? scale)
974987
{
988+
// TODO: Reimplement over ParseStoreTypeName below
989+
975990
if (storeTypeName is null)
976991
{
977992
return null;
@@ -1056,4 +1071,155 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan<char> span)
10561071

10571072
return new StringBuilder(preParens.Length).Append(preParens).Append(postParens).ToString();
10581073
}
1074+
1075+
internal static void ParseStoreTypeName(
1076+
string storeTypeName,
1077+
out string name,
1078+
out string? schema,
1079+
out int? size,
1080+
out int? precision,
1081+
out int? scale)
1082+
{
1083+
var s = storeTypeName.AsSpan().Trim();
1084+
var i = 0;
1085+
size = precision = scale = null;
1086+
1087+
if (s.EndsWith("[]", StringComparison.Ordinal))
1088+
{
1089+
// If this is an array store type, any facets (size, precision...) apply to the element and not to the array (e.g. varchar(32)[]
1090+
// is an array mapping with Size=null over an element mapping of varchar with Size=32). So just add everything up to the end.
1091+
// Note that if there's a schema (e.g. foo.varchar(32)[]), we return name=varchar(32), schema=foo.
1092+
name = s.ToString();
1093+
schema = null;
1094+
return;
1095+
}
1096+
1097+
name = ParseNameComponent(s);
1098+
1099+
if (i < s.Length && s[i] == '.')
1100+
{
1101+
i++;
1102+
schema = name;
1103+
name = ParseNameComponent(s);
1104+
}
1105+
else
1106+
{
1107+
schema = null;
1108+
}
1109+
1110+
s = s[i..];
1111+
1112+
if (s.Length == 0 || s[0] != '(')
1113+
{
1114+
// No facets
1115+
return;
1116+
}
1117+
1118+
s = s[1..];
1119+
1120+
var closeParen = s.IndexOf(")", StringComparison.Ordinal);
1121+
if (closeParen == -1)
1122+
{
1123+
return;
1124+
}
1125+
1126+
var inParens = s[..closeParen].Trim();
1127+
// There may be stuff after the closing parentheses (e.g. timestamp(3) with time zone)
1128+
var postParens = s.Slice(closeParen + 1);
1129+
1130+
switch (s.IndexOf(",", StringComparison.Ordinal))
1131+
{
1132+
// No comma inside the parentheses, parse the value either as size or precision
1133+
case -1:
1134+
if (!int.TryParse(inParens, out var p))
1135+
{
1136+
return;
1137+
}
1138+
1139+
if (NameBasesUsesPrecision(name))
1140+
{
1141+
precision = p;
1142+
// scale = 0;
1143+
}
1144+
else
1145+
{
1146+
size = p;
1147+
}
1148+
1149+
break;
1150+
1151+
case var comma:
1152+
if (int.TryParse(s[..comma].Trim(), out var parsedPrecision))
1153+
{
1154+
precision = parsedPrecision;
1155+
}
1156+
else
1157+
{
1158+
return;
1159+
}
1160+
1161+
if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale))
1162+
{
1163+
scale = parsedScale;
1164+
}
1165+
else
1166+
{
1167+
return;
1168+
}
1169+
1170+
break;
1171+
}
1172+
1173+
if (postParens.Length > 0)
1174+
{
1175+
// There's stuff after the parentheses (e.g. time(3) with time zone), append to the name
1176+
name += postParens.ToString();
1177+
}
1178+
1179+
string ParseNameComponent(ReadOnlySpan<char> s)
1180+
{
1181+
var inQuotes = false;
1182+
StringBuilder builder = new();
1183+
1184+
if (s[i] == '"')
1185+
{
1186+
inQuotes = true;
1187+
i++;
1188+
}
1189+
1190+
var start = i;
1191+
1192+
for (; i < s.Length; i++)
1193+
{
1194+
var c = s[i];
1195+
1196+
if (inQuotes)
1197+
{
1198+
if (c == '"')
1199+
{
1200+
if (i + 1 < s.Length && s[i + 1] == '"')
1201+
{
1202+
builder.Append('"');
1203+
i++;
1204+
continue;
1205+
}
1206+
1207+
i++;
1208+
break;
1209+
}
1210+
}
1211+
else if (!char.IsWhiteSpace(c) && !char.IsAsciiLetterOrDigit(c) && c != '_')
1212+
{
1213+
break;
1214+
}
1215+
1216+
builder.Append(c);
1217+
}
1218+
1219+
var length = i - start;
1220+
return length == storeTypeName.Length
1221+
? storeTypeName
1222+
: builder.ToString();
1223+
}
1224+
}
10591225
}

0 commit comments

Comments
 (0)