Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public abstract class TypeHandlerResolverFactory
public virtual TypeMappingResolver? CreateMappingResolver() => null;

public virtual TypeMappingResolver? CreateGlobalMappingResolver() => null;

public virtual TypeHandlerResolverFactoryPosition Position => TypeHandlerResolverFactoryPosition.Beginning;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Npgsql.Internal.TypeHandling;

public enum TypeHandlerResolverFactoryPosition
{
Beginning,
End
}
55 changes: 1 addition & 54 deletions src/Npgsql/Internal/TypeMapping/TypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal void Initialize(
{
var globalMappingResolver = resolverFactories[i].CreateGlobalMappingResolver();
if (globalMappingResolver is not null)
GlobalTypeMapper.Instance.TryAddMappingResolver(globalMappingResolver);
GlobalTypeMapper.Instance.TryAddMappingResolver(globalMappingResolver, resolverFactories[i].Position);
}

_handlerResolvers = handlerResolvers;
Expand Down Expand Up @@ -200,18 +200,6 @@ NpgsqlTypeHandler ResolveLong(NpgsqlDbType npgsqlDbType)
}
}

if (npgsqlDbType.HasFlag(NpgsqlDbType.Array))
{
var elementHandler = ResolveByNpgsqlDbType(npgsqlDbType & ~NpgsqlDbType.Array);

if (elementHandler.PostgresType.Array is not { } pgArrayType)
throw new ArgumentException(
$"No array type could be found in the database for element {elementHandler.PostgresType}");

return _handlersByNpgsqlDbType[npgsqlDbType] =
elementHandler.CreateArrayHandler(pgArrayType, Connector.Settings.ArrayNullabilityMode);
}

throw new NpgsqlException($"The NpgsqlDbType '{npgsqlDbType}' isn't present in your database. " +
"You may need to install an extension or upgrade to a newer version.");
}
Expand Down Expand Up @@ -287,13 +275,6 @@ internal NpgsqlTypeHandler ResolveByDataTypeName(string typeName)

switch (pgType)
{
case PostgresArrayType pgArrayType:
{
var elementHandler = ResolveByOID(pgArrayType.Element.OID);
return _handlersByDataTypeName[typeName] =
elementHandler.CreateArrayHandler(pgArrayType, Connector.Settings.ArrayNullabilityMode);
}

case PostgresEnumType pgEnumType:
{
// A mapped enum would have been registered in _extraHandlersByDataTypeName and bound above - this is unmapped.
Expand Down Expand Up @@ -430,21 +411,6 @@ NpgsqlTypeHandler ResolveLong(Type type)
}
}

// Try to see if it is an array type
var arrayElementType = GetArrayListElementType(type);
if (arrayElementType is not null)
{
if (ResolveByClrType(arrayElementType) is not { } elementHandler)
throw new ArgumentException($"Array type over CLR type {arrayElementType.Name} isn't supported by Npgsql");

if (elementHandler.PostgresType.Array is not { } pgArrayType)
throw new ArgumentException(
$"No array type could be found in the database for element {elementHandler.PostgresType}");

return _handlersByClrType[type] =
elementHandler.CreateArrayHandler(pgArrayType, Connector.Settings.ArrayNullabilityMode);
}

if (Nullable.GetUnderlyingType(type) is { } underlyingType && ResolveByClrType(underlyingType) is { } underlyingHandler)
return _handlersByClrType[type] = underlyingHandler;

Expand All @@ -464,25 +430,6 @@ NpgsqlTypeHandler ResolveLong(Type type)
throw new NotSupportedException($"The CLR type {type} isn't natively supported by Npgsql or your PostgreSQL. " +
$"To use it with a PostgreSQL composite you need to specify {nameof(NpgsqlParameter.DataTypeName)} or to map it, please refer to the documentation.");
}

static Type? GetArrayListElementType(Type type)
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsArray)
return GetUnderlyingType(type.GetElementType()!); // The use of bang operator is justified here as Type.GetElementType() only returns null for the Array base class which can't be mapped in a useful way.

var ilist = typeInfo.ImplementedInterfaces.FirstOrDefault(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>));
if (ilist != null)
return GetUnderlyingType(ilist.GetGenericArguments()[0]);

if (typeof(IList).IsAssignableFrom(type))
throw new NotSupportedException("Non-generic IList is a supported parameter, but the NpgsqlDbType parameter must be set on the parameter");

return null;

Type GetUnderlyingType(Type t)
=> Nullable.GetUnderlyingType(t) ?? t;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Npgsql/NpgsqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,6 @@ void AddDefaultFeatures()
_internalBuilder.AddDefaultTypeResolverFactory(new SystemTextJsonTypeHandlerResolverFactory());
_internalBuilder.AddDefaultTypeResolverFactory(new RangeTypeHandlerResolverFactory());
_internalBuilder.AddDefaultTypeResolverFactory(new RecordTypeHandlerResolverFactory());
_internalBuilder.AddDefaultTypeResolverFactory(new ArrayTypeHandlerResolverFactory());
}
}
20 changes: 19 additions & 1 deletion src/Npgsql/NpgsqlSlimDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ public void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory)
}
}

_resolverFactories.Insert(0, resolverFactory);
if (resolverFactory.Position == TypeHandlerResolverFactoryPosition.End)
_resolverFactories.Add(resolverFactory);
else
_resolverFactories.Insert(0, resolverFactory);
}

internal void AddDefaultTypeResolverFactory(TypeHandlerResolverFactory resolverFactory)
Expand All @@ -267,6 +270,12 @@ internal void AddDefaultTypeResolverFactory(TypeHandlerResolverFactory resolverF
if (_resolverFactories[i].GetType() == type)
return;

if (resolverFactory.Position == TypeHandlerResolverFactoryPosition.End)
{
_resolverFactories.Add(resolverFactory);
return;
}

for (var i = 0; i < _resolverFactories.Count; i++)
{
if (_resolverFactories[i] is BuiltInTypeHandlerResolverFactory)
Expand Down Expand Up @@ -417,6 +426,15 @@ public NpgsqlSlimDataSourceBuilder EnableRecords()
return this;
}

/// <summary>
/// Sets up mappings for the arrays.
/// </summary>
public NpgsqlSlimDataSourceBuilder EnableArrays()
{
AddTypeResolverFactory(new ArrayTypeHandlerResolverFactory());
return this;
}

/// <summary>
/// Enables the possibility to use TLS/SSl encryption for connections to PostgreSQL. This does not guarantee that encryption will
/// actually be used; see <see href="https://www.npgsql.org/doc/security.html"/> for more details.
Expand Down
1 change: 1 addition & 0 deletions src/Npgsql/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
Npgsql.NpgsqlSlimDataSourceBuilder.EnableArrays() -> Npgsql.NpgsqlSlimDataSourceBuilder!
Npgsql.NpgsqlSlimDataSourceBuilder.EnableRecords() -> Npgsql.NpgsqlSlimDataSourceBuilder!
override Npgsql.NpgsqlBatch.Dispose() -> void
Npgsql.NpgsqlBinaryImporter.WriteRow(params object?[]! values) -> void
Expand Down
112 changes: 112 additions & 0 deletions src/Npgsql/TypeMapping/ArrayTypeHandlerResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandlers.InternalTypeHandlers;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;
using Npgsql.PostgresTypes;
using NpgsqlTypes;

namespace Npgsql.TypeMapping;

sealed class ArrayTypeHandlerResolver : TypeHandlerResolver
{
readonly TypeMapper _typeMapper;
readonly NpgsqlConnector _connector;
readonly NpgsqlDatabaseInfo _databaseInfo;

// Internal types
Int2VectorHandler? _int2VectorHandler;
OIDVectorHandler? _oidVectorHandler;

// Complex type handlers over timestamp/timestamptz (because DateTime is value-dependent)
NpgsqlTypeHandler? _timestampArrayHandler;
NpgsqlTypeHandler? _timestampTzArrayHandler;

public ArrayTypeHandlerResolver(TypeMapper typeMapper, NpgsqlConnector connector)
{
_typeMapper = typeMapper;
_connector = connector;
_databaseInfo = connector.DatabaseInfo;
}

public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
{
if (typeName == "int2vector")
return Int2VectorHandler();
if (typeName == "oidvector")
return OidVectorHandler();

if (_databaseInfo.TryGetPostgresTypeByName(typeName, out var pgType) && pgType is PostgresArrayType pgArrayType)
return _typeMapper.ResolveByOID(pgArrayType.Element.OID).CreateArrayHandler(pgArrayType, _connector.Settings.ArrayNullabilityMode);

return null;
}

public override NpgsqlTypeHandler? ResolveByNpgsqlDbType(NpgsqlDbType npgsqlDbType)
{
if (!npgsqlDbType.HasFlag(NpgsqlDbType.Array))
return null;

var elementHandler = _typeMapper.ResolveByNpgsqlDbType(npgsqlDbType & ~NpgsqlDbType.Array);

if (elementHandler.PostgresType.Array is not { } pgArrayType)
return null;

return elementHandler.CreateArrayHandler(pgArrayType, _connector.Settings.ArrayNullabilityMode);
}

public override NpgsqlTypeHandler? ResolveValueDependentValue(object value)
{
// For arrays/lists, return timestamp or timestamptz based on the kind of the first DateTime; if the user attempts to
// mix incompatible Kinds, that will fail during validation. For empty arrays it doesn't matter.
if (value is IList<DateTime> array)
return ArrayHandler(array.Count == 0 ? DateTimeKind.Unspecified : array[0].Kind);

return null;

NpgsqlTypeHandler ArrayHandler(DateTimeKind kind)
=> kind == DateTimeKind.Utc
? _timestampTzArrayHandler ??= _typeMapper.ResolveByNpgsqlDbType(NpgsqlDbType.TimestampTz).CreateArrayHandler(
(PostgresArrayType)PgType("timestamp with time zone[]"), _connector.Settings.ArrayNullabilityMode)
: _timestampArrayHandler ??= _typeMapper.ResolveByNpgsqlDbType(NpgsqlDbType.Timestamp).CreateArrayHandler(
(PostgresArrayType)PgType("timestamp without time zone[]"), _connector.Settings.ArrayNullabilityMode);
}

public override NpgsqlTypeHandler? ResolveByClrType(Type type)
{
// Try to see if it is an array type
var arrayElementType = GetArrayListElementType(type);
if (arrayElementType is null)
return null;

if (_typeMapper.ResolveByClrType(arrayElementType) is not { } elementHandler)
return null;

if (elementHandler.PostgresType.Array is not { } pgArrayType)
return null;

return elementHandler.CreateArrayHandler(pgArrayType, _connector.Settings.ArrayNullabilityMode);
}

static Type? GetArrayListElementType(Type type)
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsArray)
return GetUnderlyingType(type.GetElementType()!); // The use of bang operator is justified here as Type.GetElementType() only returns null for the Array base class which can't be mapped in a useful way.

var ilist = typeInfo.ImplementedInterfaces.FirstOrDefault(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>));
return ilist != null ? GetUnderlyingType(ilist.GetGenericArguments()[0]) : null;

Type GetUnderlyingType(Type t)
=> Nullable.GetUnderlyingType(t) ?? t;
}

NpgsqlTypeHandler Int2VectorHandler() => _int2VectorHandler ??= new Int2VectorHandler(PgType("int2vector"), PgType("smallint"));
NpgsqlTypeHandler OidVectorHandler() => _oidVectorHandler ??= new OIDVectorHandler(PgType("oidvector"), PgType("oid"));

PostgresType PgType(string pgTypeName) => _databaseInfo.GetPostgresTypeByName(pgTypeName);
}
17 changes: 17 additions & 0 deletions src/Npgsql/TypeMapping/ArrayTypeHandlerResolverFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Npgsql.Internal;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;

namespace Npgsql.TypeMapping;

sealed class ArrayTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
public override TypeHandlerResolver Create(TypeMapper typeMapper, NpgsqlConnector connector)
=> new ArrayTypeHandlerResolver(typeMapper, connector);

public override TypeMappingResolver CreateMappingResolver() => new ArrayTypeMappingResolver();

public override TypeMappingResolver CreateGlobalMappingResolver() => new ArrayTypeMappingResolver();

public override TypeHandlerResolverFactoryPosition Position => TypeHandlerResolverFactoryPosition.End;
}
44 changes: 44 additions & 0 deletions src/Npgsql/TypeMapping/ArrayTypeMappingResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;
using Npgsql.PostgresTypes;
using NpgsqlTypes;
using static Npgsql.Util.Statics;

namespace Npgsql.TypeMapping;

sealed class ArrayTypeMappingResolver : TypeMappingResolver
{
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) =>
dataTypeName switch
{
"int2vector" => new(NpgsqlDbType.Int2Vector, "int2vector"),
"oidvector" => new(NpgsqlDbType.Oidvector, "oidvector"),
"timestamp without time zone[]" => new(NpgsqlDbType.Array | NpgsqlDbType.Timestamp, "timestamp without time zone[]"),
"timestamp with time zone[]" => new(NpgsqlDbType.Array | NpgsqlDbType.TimestampTz, "timestamp with time zone[]"),
_ => null
};

public override string? GetDataTypeNameByClrType(Type clrType) => null;

public override TypeMappingInfo? GetMappingByPostgresType(TypeMapper typeMapper, PostgresType type) => GetMappingByDataTypeName(type.Name);

public override string? GetDataTypeNameByValueDependentValue(object value)
{
// In LegacyTimestampBehavior, DateTime isn't value-dependent, and handled above in ClrTypeToDataTypeNameTable like other types
if (LegacyTimestampBehavior)
return null;

// For arrays/lists, return timestamp or timestamptz based on the kind of the first DateTime; if the user attempts to
// mix incompatible Kinds, that will fail during validation. For empty arrays it doesn't matter.
if (value is IList<DateTime> array)
return array.Count == 0
? "timestamp without time zone[]"
: array[0].Kind == DateTimeKind.Utc
? "timestamp with time zone[]"
: "timestamp without time zone[]";

return null;
}
}
21 changes: 0 additions & 21 deletions src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,13 @@ sealed class BuiltInTypeHandlerResolver : TypeHandlerResolver
HstoreHandler? _hstoreHandler;

// Internal types
Int2VectorHandler? _int2VectorHandler;
OIDVectorHandler? _oidVectorHandler;
PgLsnHandler? _pgLsnHandler;
TidHandler? _tidHandler;
InternalCharHandler? _internalCharHandler;

// Special types
UnknownTypeHandler? _unknownHandler;

// Complex type handlers over timestamp/timestamptz (because DateTime is value-dependent)
NpgsqlTypeHandler? _timestampArrayHandler;
NpgsqlTypeHandler? _timestampTzArrayHandler;

#endregion Cached handlers

internal BuiltInTypeHandlerResolver(NpgsqlConnector connector)
Expand Down Expand Up @@ -205,8 +199,6 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector)
"hstore" => HstoreHandler(),

// Internal types
"int2vector" => Int2VectorHandler(),
"oidvector" => OidVectorHandler(),
"pg_lsn" => PgLsnHandler(),
"tid" => TidHandler(),
"char" => InternalCharHandler(),
Expand Down Expand Up @@ -241,19 +233,8 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector)
{
DateTime dateTime => dateTime.Kind == DateTimeKind.Utc ? _timestampTzHandler : _timestampHandler,

// For arrays/lists, return timestamp or timestamptz based on the kind of the first DateTime; if the user attempts to
// mix incompatible Kinds, that will fail during validation. For empty arrays it doesn't matter.
IList<DateTime> array => ArrayHandler(array.Count == 0 ? DateTimeKind.Unspecified : array[0].Kind),

_ => null
};

NpgsqlTypeHandler ArrayHandler(DateTimeKind kind)
=> kind == DateTimeKind.Utc
? _timestampTzArrayHandler ??= _timestampTzHandler.CreateArrayHandler(
(PostgresArrayType)PgType("timestamp with time zone[]"), _connector.Settings.ArrayNullabilityMode)
: _timestampArrayHandler ??= _timestampHandler.CreateArrayHandler(
(PostgresArrayType)PgType("timestamp without time zone[]"), _connector.Settings.ArrayNullabilityMode);
}

public override NpgsqlTypeHandler? ResolveValueTypeGenerically<T>(T value)
Expand Down Expand Up @@ -417,8 +398,6 @@ NpgsqlTypeHandler ArrayHandler(DateTimeKind kind)
: null;

// Internal types
NpgsqlTypeHandler Int2VectorHandler() => _int2VectorHandler ??= new Int2VectorHandler(PgType("int2vector"), PgType("smallint"));
NpgsqlTypeHandler OidVectorHandler() => _oidVectorHandler ??= new OIDVectorHandler(PgType("oidvector"), PgType("oid"));
NpgsqlTypeHandler PgLsnHandler() => _pgLsnHandler ??= new PgLsnHandler(PgType("pg_lsn"));
NpgsqlTypeHandler TidHandler() => _tidHandler ??= new TidHandler(PgType("tid"));
NpgsqlTypeHandler InternalCharHandler() => _internalCharHandler ??= new InternalCharHandler(PgType("char"));
Expand Down
Loading