Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions EFCore.PG.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EXml_002ECodeStyle_002EFormatSettingsUpgrade_002EXmlMoveToCommonFormatterSettingsUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=noda/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=timestamptz/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
190 changes: 178 additions & 12 deletions src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 })
{
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -972,6 +985,8 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan<char> span)
ref int? precision,
ref int? scale)
{
// TODO: Reimplement over ParseStoreTypeName below

if (storeTypeName is null)
{
return null;
Expand Down Expand Up @@ -1056,4 +1071,155 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan<char> 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<char> 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();
}
}
}
Loading
Loading