Skip to content

Commit

Permalink
Bring UnsupportedTypeInfoResolver up-to-date with current mechanics (#…
Browse files Browse the repository at this point in the history
…5556)

Fixes #5550

(cherry picked from commit 7f8c257)
  • Loading branch information
NinoFloris committed Feb 5, 2024
1 parent 5d4609d commit 7691c95
Show file tree
Hide file tree
Showing 21 changed files with 221 additions and 246 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Npgsql.Internal;

sealed class TypeInfoResolverChain : IPgTypeInfoResolver
sealed class ChainTypeInfoResolver : IPgTypeInfoResolver
{
readonly IPgTypeInfoResolver[] _resolvers;

public TypeInfoResolverChain(IEnumerable<IPgTypeInfoResolver> resolvers)
public ChainTypeInfoResolver(IEnumerable<IPgTypeInfoResolver> resolvers)
=> _resolvers = new List<IPgTypeInfoResolver>(resolvers).ToArray();

public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
Expand Down
15 changes: 13 additions & 2 deletions src/Npgsql/Internal/PgSerializerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ public sealed class PgSerializerOptions
[field: ThreadStatic]
internal static bool IntrospectionCaller { get; set; }

readonly PgTypeInfoResolverChain _resolverChain;
readonly Func<string>? _timeZoneProvider;
IPgTypeInfoResolver? _typeInfoResolver;
object? _typeInfoCache;

internal PgSerializerOptions(NpgsqlDatabaseInfo databaseInfo, Func<string>? timeZoneProvider = null)
internal PgSerializerOptions(NpgsqlDatabaseInfo databaseInfo, PgTypeInfoResolverChain? resolverChain = null, Func<string>? timeZoneProvider = null)
{
_resolverChain = resolverChain ?? new();
_timeZoneProvider = timeZoneProvider;
DatabaseInfo = databaseInfo;
UnknownPgType = databaseInfo.GetPostgresType("unknown");
Expand All @@ -42,7 +45,11 @@ internal bool IntrospectionMode

public string TimeZone => _timeZoneProvider?.Invoke() ?? throw new NotSupportedException("TimeZone was not configured.");
public Encoding TextEncoding { get; init; } = Encoding.UTF8;
public required IPgTypeInfoResolver TypeInfoResolver { get; init; }
public IPgTypeInfoResolver TypeInfoResolver
{
get => _typeInfoResolver ??= new ChainTypeInfoResolver(_resolverChain);
internal init => _typeInfoResolver = value;
}
public bool EnableDateTimeInfinityConversions { get; init; } = true;

public ArrayNullabilityMode ArrayNullabilityMode { get; init; } = ArrayNullabilityMode.Never;
Expand All @@ -54,6 +61,10 @@ internal bool IntrospectionMode
typeof(char), typeof(char?)
};

internal bool RangesEnabled => _resolverChain.RangesEnabled;
internal bool MultirangesEnabled => _resolverChain.MultirangesEnabled;
internal bool ArraysEnabled => _resolverChain.ArraysEnabled;

// We don't verify the kind of pgTypeId we get, it'll throw if it's incorrect.
// It's up to the caller to call GetCanonicalTypeId if they want to use an oid instead of a DataTypeName.
// This also makes it easier to realize it should be a cached value if infos for different CLR types are requested for the same
Expand Down
48 changes: 43 additions & 5 deletions src/Npgsql/Internal/PgTypeInfoResolverChainBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Npgsql.Internal;
Expand All @@ -10,7 +11,7 @@ struct PgTypeInfoResolverChainBuilder
Action<PgTypeInfoResolverChainBuilder, List<IPgTypeInfoResolver>>? _addMultirangeResolvers;
RangeArrayHandler _rangeArrayHandler = RangeArrayHandler.Instance;
MultirangeArrayHandler _multirangeArrayHandler = MultirangeArrayHandler.Instance;
Action<PgTypeInfoResolverChainBuilder, List<IPgTypeInfoResolver>>? _arrayResolvers;
Action<PgTypeInfoResolverChainBuilder, List<IPgTypeInfoResolver>>? _addArrayResolvers;

public PgTypeInfoResolverChainBuilder()
{
Expand Down Expand Up @@ -66,7 +67,7 @@ static void AddResolvers(PgTypeInfoResolverChainBuilder instance, List<IPgTypeIn

public void EnableArrays()
{
_arrayResolvers ??= AddResolvers;
_addArrayResolvers ??= AddResolvers;

static void AddResolvers(PgTypeInfoResolverChainBuilder instance, List<IPgTypeInfoResolver> resolvers)
{
Expand All @@ -86,17 +87,22 @@ static void AddResolvers(PgTypeInfoResolverChainBuilder instance, List<IPgTypeIn
}
}

public IEnumerable<IPgTypeInfoResolver> Build(Action<List<IPgTypeInfoResolver>>? configure = null)
public PgTypeInfoResolverChain Build(Action<List<IPgTypeInfoResolver>>? configure = null)
{
var resolvers = new List<IPgTypeInfoResolver>();
foreach (var factory in _factories)
resolvers.Add(factory.CreateResolver());
var instance = this;
_addRangeResolvers?.Invoke(instance, resolvers);
_addMultirangeResolvers?.Invoke(instance, resolvers);
_arrayResolvers?.Invoke(instance, resolvers);
_addArrayResolvers?.Invoke(instance, resolvers);
configure?.Invoke(resolvers);
return resolvers;
return new(
resolvers,
rangesEnabled: _addRangeResolvers is not null,
multirangesEnabled: _addMultirangeResolvers is not null,
arraysEnabled: _addArrayResolvers is not null
);
}

class RangeArrayHandler
Expand Down Expand Up @@ -127,3 +133,35 @@ sealed class MultirangeArrayHandlerImpl : MultirangeArrayHandler
public override IPgTypeInfoResolver? CreateMultirangeArrayResolver(PgTypeInfoResolverFactory factory) => factory.CreateMultirangeArrayResolver();
}
}

readonly struct PgTypeInfoResolverChain : IEnumerable<IPgTypeInfoResolver>
{
[Flags]
enum EnabledFlags
{
None = 0,
Ranges = 1,
Multiranges = 2,
Arrays = 4
}

readonly EnabledFlags _enabled;
readonly List<IPgTypeInfoResolver> _resolvers;

public PgTypeInfoResolverChain(List<IPgTypeInfoResolver> resolvers, bool rangesEnabled, bool multirangesEnabled, bool arraysEnabled)
{
_enabled = rangesEnabled ? EnabledFlags.Ranges | _enabled : _enabled;
_enabled = multirangesEnabled ? EnabledFlags.Multiranges | _enabled : _enabled;
_enabled = arraysEnabled ? EnabledFlags.Arrays | _enabled : _enabled;
_resolvers = resolvers;
}

public bool RangesEnabled => _enabled.HasFlag(EnabledFlags.Ranges);
public bool MultirangesEnabled => _enabled.HasFlag(EnabledFlags.Multiranges);
public bool ArraysEnabled => _enabled.HasFlag(EnabledFlags.Arrays);

public IEnumerator<IPgTypeInfoResolver> GetEnumerator()
=> _resolvers?.GetEnumerator() ?? (IEnumerator<IPgTypeInfoResolver>)Array.Empty<IPgTypeInfoResolver>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _resolvers?.GetEnumerator() ?? Array.Empty<IPgTypeInfoResolver>().GetEnumerator();
}
3 changes: 2 additions & 1 deletion src/Npgsql/Internal/Postgres/DataTypeName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ internal static DataTypeName ValidatedName(string fullyQualifiedDataTypeName)

public string UnqualifiedDisplayName => ToDisplayName(UnqualifiedNameSpan);

internal ReadOnlySpan<char> SchemaSpan => Value.AsSpan(0, _value.IndexOf('.'));
public string Schema => Value.Substring(0, _value.IndexOf('.'));
internal ReadOnlySpan<char> UnqualifiedNameSpan => Value.AsSpan().Slice(_value.IndexOf('.') + 1);
internal ReadOnlySpan<char> UnqualifiedNameSpan => Value.AsSpan(_value.IndexOf('.') + 1);
public string UnqualifiedName => Value.Substring(_value.IndexOf('.') + 1);
public string Value => _value is null ? ThrowDefaultException() : _value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System.Collections.Generic;
using Npgsql.Internal.Converters;
using Npgsql.Internal.Postgres;
using Npgsql.PostgresTypes;
using Npgsql.Properties;
using Npgsql.Util;
using NpgsqlTypes;
using static Npgsql.Internal.PgConverterFactory;
Expand All @@ -15,71 +13,6 @@ sealed partial class AdoTypeInfoResolverFactory
public override IPgTypeInfoResolver CreateMultirangeResolver() => new MultirangeResolver();
public override IPgTypeInfoResolver CreateMultirangeArrayResolver() => new MultirangeArrayResolver();

public static void ThrowIfMultirangeUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
var kind = CheckMultirangeUnsupported(type, dataTypeName, options);
switch (kind)
{
case PostgresTypeKind.Multirange when kind.Value.HasFlag(PostgresTypeKind.Array):
throw new NotSupportedException(
string.Format(NpgsqlStrings.MultirangeArraysNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableArrays), typeof(TBuilder).Name));
case PostgresTypeKind.Multirange:
throw new NotSupportedException(
string.Format(NpgsqlStrings.MultirangesNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableMultiranges), typeof(TBuilder).Name));
default:
return;
}
}

public static PostgresTypeKind? CheckMultirangeUnsupported(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
// Only trigger on well known data type names.
var npgsqlDbType = dataTypeName?.ToNpgsqlDbType();
if (type != typeof(object))
{
if (npgsqlDbType?.HasFlag(NpgsqlDbType.Multirange) != true)
return null;

return dataTypeName?.IsArray == true
? PostgresTypeKind.Array | PostgresTypeKind.Multirange
: PostgresTypeKind.Multirange;
}

if (type == typeof(object))
return null;

if (!TypeInfoMappingCollection.IsArrayLikeType(type, out var elementType))
return null;

type = elementType;

if (type is { IsConstructedGenericType: true } && type.GetGenericTypeDefinition() == typeof(Nullable<>))
type = type.GetGenericArguments()[0];

if (type is { IsConstructedGenericType: true } && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>))
{
type = type.GetGenericArguments()[0];
var matchingArguments =
new[]
{
typeof(int), typeof(long), typeof(decimal), typeof(DateTime),
# if NET6_0_OR_GREATER
typeof(DateOnly)
#endif
};

// If we don't know more than the clr type, default to a Multirange kind over Array as they share the same types.
foreach (var argument in matchingArguments)
if (argument == type)
return PostgresTypeKind.Multirange;

if (type.AssemblyQualifiedName == "System.Numerics.BigInteger,System.Runtime.Numerics")
return PostgresTypeKind.Multirange;
}

return null;
}

class MultirangeResolver : IPgTypeInfoResolver
{
TypeInfoMappingCollection? _mappings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System.Numerics;
using Npgsql.Internal.Converters;
using Npgsql.Internal.Postgres;
using Npgsql.PostgresTypes;
using Npgsql.Properties;
using Npgsql.Util;
using NpgsqlTypes;
using static Npgsql.Internal.PgConverterFactory;
Expand All @@ -15,71 +13,6 @@ sealed partial class AdoTypeInfoResolverFactory
public override IPgTypeInfoResolver CreateRangeResolver() => new RangeResolver();
public override IPgTypeInfoResolver CreateRangeArrayResolver() => new RangeArrayResolver();

public static void ThrowIfRangeUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
var kind = CheckRangeUnsupported(type, dataTypeName, options);
switch (kind)
{
case PostgresTypeKind.Range when kind.Value.HasFlag(PostgresTypeKind.Array):
throw new NotSupportedException(
string.Format(NpgsqlStrings.RangeArraysNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableArrays), typeof(TBuilder).Name));
case PostgresTypeKind.Range:
throw new NotSupportedException(
string.Format(NpgsqlStrings.RangesNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableRanges), typeof(TBuilder).Name));
default:
return;
}
}

public static PostgresTypeKind? CheckRangeUnsupported(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
// Only trigger on well known data type names.
var npgsqlDbType = dataTypeName?.ToNpgsqlDbType();
if (type != typeof(object))
{
if (npgsqlDbType?.HasFlag(NpgsqlDbType.Range) != true && npgsqlDbType?.HasFlag(NpgsqlDbType.Multirange) != true)
return null;

if (npgsqlDbType.Value.HasFlag(NpgsqlDbType.Range))
return dataTypeName?.IsArray == true
? PostgresTypeKind.Array | PostgresTypeKind.Range
: PostgresTypeKind.Range;

return dataTypeName?.IsArray == true
? PostgresTypeKind.Array | PostgresTypeKind.Multirange
: PostgresTypeKind.Multirange;
}

if (type == typeof(object))
return null;

if (type is { IsConstructedGenericType: true } && type.GetGenericTypeDefinition() == typeof(Nullable<>))
type = type.GetGenericArguments()[0];

if (type is { IsConstructedGenericType: true } && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>))
{
type = type.GetGenericArguments()[0];
var matchingArguments =
new[]
{
typeof(int), typeof(long), typeof(decimal), typeof(DateTime),
# if NET6_0_OR_GREATER
typeof(DateOnly)
#endif
};

// If we don't know more than the clr type, default to a Multirange kind over Array as they share the same types.
foreach (var argument in matchingArguments)
if (argument == type)
return PostgresTypeKind.Range;

if (type.AssemblyQualifiedName == "System.Numerics.BigInteger,System.Runtime.Numerics")
return PostgresTypeKind.Range;
}

return null;
}

class RangeResolver : IPgTypeInfoResolver
{
TypeInfoMappingCollection? _mappings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ sealed class FullTextSearchTypeInfoResolverFactory : PgTypeInfoResolverFactory
public override IPgTypeInfoResolver CreateResolver() => new Resolver();
public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver();

public static void CheckUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
public static void ThrowIfUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (type != typeof(object) && (dataTypeName == DataTypeNames.TsQuery || dataTypeName == DataTypeNames.TsVector))
if (dataTypeName is { SchemaSpan: "pg_catalog", UnqualifiedNameSpan: "tsquery" or "_tsquery" or "tsvector" or "_tsvector" })
throw new NotSupportedException(
string.Format(NpgsqlStrings.FullTextSearchNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableFullTextSearch), typeof(TBuilder).Name));

Expand All @@ -23,8 +23,8 @@ public static void CheckUnsupported<TBuilder>(Type? type, DataTypeName? dataType
if (TypeInfoMappingCollection.IsArrayLikeType(type, out var elementType))
type = elementType;

if (type is { IsConstructedGenericType: true } && type.GetGenericTypeDefinition() == typeof(Nullable<>))
type = type.GetGenericArguments()[0];
if (Nullable.GetUnderlyingType(type) is { } underlyingType)
type = underlyingType;

if (type == typeof(NpgsqlTsVector) || typeof(NpgsqlTsQuery).IsAssignableFrom(type))
throw new NotSupportedException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.Json.Serialization.Metadata;
using Npgsql.Internal.Converters;
using Npgsql.Internal.Postgres;
using Npgsql.Properties;

namespace Npgsql.Internal.ResolverFactories;

Expand All @@ -27,6 +28,17 @@ public JsonDynamicTypeInfoResolverFactory(Type[]? jsonbClrTypes = null, Type[]?
public override IPgTypeInfoResolver CreateResolver() => new Resolver(_jsonbClrTypes, _jsonClrTypes, _serializerOptions);
public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver(_jsonbClrTypes, _jsonClrTypes, _serializerOptions);

public static void ThrowIfUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (dataTypeName is { SchemaSpan: "pg_catalog", UnqualifiedNameSpan: "json" or "_json" or "jsonb" or "_jsonb" })
throw new NotSupportedException(
string.Format(
NpgsqlStrings.DynamicJsonNotEnabled,
type is null || type == typeof(object) ? "<unknown>" : type.Name,
nameof(NpgsqlSlimDataSourceBuilder.EnableDynamicJson),
typeof(TBuilder).Name));
}

[RequiresUnreferencedCode("Json serializer may perform reflection on trimmed types.")]
[RequiresDynamicCode("Serializing arbitrary types to json can require creating new generic types or methods, which requires creating code at runtime. This may not work when AOT compiling.")]
class Resolver : DynamicTypeInfoResolver, IPgTypeInfoResolver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ sealed class LTreeTypeInfoResolverFactory : PgTypeInfoResolverFactory
public override IPgTypeInfoResolver CreateResolver() => new Resolver();
public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver();

public static void CheckUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
public static void ThrowIfUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (type != typeof(object) && dataTypeName is { UnqualifiedName: "ltree" or "lquery" or "ltxtquery" })
if (dataTypeName is { UnqualifiedNameSpan: "ltree" or "_ltree" or "lquery" or "_lquery" or "ltxtquery" or "_ltxtquery" })
throw new NotSupportedException(
string.Format(NpgsqlStrings.LTreeNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableLTree),
typeof(TBuilder).Name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ sealed class RecordTypeInfoResolverFactory : PgTypeInfoResolverFactory
public override IPgTypeInfoResolver CreateResolver() => new Resolver();
public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver();

public static void CheckUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
public static void ThrowIfUnsupported<TBuilder>(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (type != typeof(object) && dataTypeName == DataTypeNames.Record)
if (dataTypeName is { SchemaSpan: "pg_catalog", UnqualifiedNameSpan: "record" or "_record" })
{
throw new NotSupportedException(
string.Format(
Expand Down

0 comments on commit 7691c95

Please sign in to comment.