diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7b7da79c8c..bd72b5fe55 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
+
diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs
index 3992f57067..e9a8bd3bea 100644
--- a/src/Npgsql/Internal/NpgsqlConnector.cs
+++ b/src/Npgsql/Internal/NpgsqlConnector.cs
@@ -567,8 +567,8 @@ internal async ValueTask LoadDatabaseInfo(bool forceReload, NpgsqlTimeout timeou
}
}
- DatabaseInfo = database!;
- TypeMapper.Bind(DatabaseInfo);
+ DatabaseInfo = TypeMapper.DatabaseInfo = database!;
+ TypeMapper.Reset();
}
internal async ValueTask QueryClusterState(
diff --git a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs
index bc21b377da..c4a746d83e 100644
--- a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs
+++ b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs
@@ -141,6 +141,33 @@ protected NpgsqlDatabaseInfo(string host, int port, string databaseName, Version
Version = version;
}
+ internal PostgresType GetPostgresTypeByName(string pgName)
+ {
+ // Full type name with namespace
+ if (pgName.IndexOf('.') > -1)
+ {
+ if (ByFullName.TryGetValue(pgName, out var pgType))
+ return pgType;
+ }
+ // No dot, partial type name
+ else if (ByName.TryGetValue(pgName, out var pgType))
+ {
+ if (pgType is not null)
+ return pgType;
+
+ // If the name was found but the value is null, that means that there are
+ // two db types with the same name (different schemas).
+ // Try to fall back to pg_catalog, otherwise fail.
+ if (ByFullName.TryGetValue($"pg_catalog.{pgName}", out pgType))
+ return pgType;
+
+ throw new ArgumentException($"More than one PostgreSQL type was found with the name {pgName}, " +
+ "please specify a full name including schema");
+ }
+
+ throw new ArgumentException($"A PostgreSQL type with the name {pgName} was not found in the database");
+ }
+
internal void ProcessTypes()
{
foreach (var type in GetTypes())
diff --git a/src/Npgsql/Internal/TypeHandlers/UnknownTypeHandler.cs b/src/Npgsql/Internal/TypeHandlers/UnknownTypeHandler.cs
index 850a894fc4..b7f46a61d2 100644
--- a/src/Npgsql/Internal/TypeHandlers/UnknownTypeHandler.cs
+++ b/src/Npgsql/Internal/TypeHandlers/UnknownTypeHandler.cs
@@ -30,12 +30,14 @@ public override ValueTask Read(NpgsqlReadBuffer buf, int byteLen, bool a
throw new Exception($"Received an unknown field but {nameof(fieldDescription)} is null (i.e. COPY mode)");
if (fieldDescription.IsBinaryFormat)
+ {
// At least get the name of the PostgreSQL type for the exception
throw new NotSupportedException(
_connector.TypeMapper.DatabaseInfo.ByOID.TryGetValue(fieldDescription.TypeOID, out var pgType)
? $"The field '{fieldDescription.Name}' has type '{pgType.DisplayName}', which is currently unknown to Npgsql. You can retrieve it as a string by marking it as unknown, please see the FAQ."
: $"The field '{fieldDescription.Name}' has a type currently unknown to Npgsql (OID {fieldDescription.TypeOID}). You can retrieve it as a string by marking it as unknown, please see the FAQ."
);
+ }
return base.Read(buf, byteLen, async, fieldDescription);
}
diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj
index e0bd7ca6a6..59f82c924e 100644
--- a/src/Npgsql/Npgsql.csproj
+++ b/src/Npgsql/Npgsql.csproj
@@ -28,5 +28,6 @@
+
diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs
index 5876f389c4..963e70c45d 100644
--- a/src/Npgsql/NpgsqlConnection.cs
+++ b/src/Npgsql/NpgsqlConnection.cs
@@ -328,6 +328,7 @@ async Task OpenAsync(bool async, CancellationToken cancellationToken)
EnlistTransaction(enlistToTransaction);
timeout = new NpgsqlTimeout(connectionTimeout);
+
// Since this connector was last used, PostgreSQL types (e.g. enums) may have been added
// (and ReloadTypes() called), or global mappings may have changed by the user.
// Bring this up to date if needed.
diff --git a/src/Npgsql/NpgsqlSchema.cs b/src/Npgsql/NpgsqlSchema.cs
index fe79fb7dc7..a051177f12 100644
--- a/src/Npgsql/NpgsqlSchema.cs
+++ b/src/Npgsql/NpgsqlSchema.cs
@@ -539,8 +539,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var baseType in connector.DatabaseInfo.BaseTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(baseType.Name, out var mapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(baseType.FullName, out mapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(baseType.Name, out var mapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(baseType.FullName, out mapping))
continue;
var row = table.Rows.Add();
@@ -556,8 +556,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var arrayType in connector.DatabaseInfo.ArrayTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(arrayType.Element.Name, out var elementMapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(arrayType.Element.FullName, out elementMapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(arrayType.Element.Name, out var elementMapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(arrayType.Element.FullName, out elementMapping))
continue;
var row = table.Rows.Add();
@@ -577,8 +577,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var rangeType in connector.DatabaseInfo.RangeTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(rangeType.Subtype.Name, out var elementMapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(rangeType.Subtype.FullName, out elementMapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(rangeType.Subtype.Name, out var elementMapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(rangeType.Subtype.FullName, out elementMapping))
continue;
var row = table.Rows.Add();
@@ -598,8 +598,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var enumType in connector.DatabaseInfo.EnumTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(enumType.Name, out var mapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(enumType.FullName, out mapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(enumType.Name, out var mapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(enumType.FullName, out mapping))
continue;
var row = table.Rows.Add();
@@ -613,8 +613,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var compositeType in connector.DatabaseInfo.CompositeTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(compositeType.Name, out var mapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(compositeType.FullName, out mapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(compositeType.Name, out var mapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(compositeType.FullName, out mapping))
continue;
var row = table.Rows.Add();
@@ -628,8 +628,8 @@ static DataTable GetDataTypes(NpgsqlConnection conn)
foreach (var domainType in connector.DatabaseInfo.DomainTypes)
{
- if (!connector.TypeMapper.InternalMappings.TryGetValue(domainType.BaseType.Name, out var baseMapping) &&
- !connector.TypeMapper.InternalMappings.TryGetValue(domainType.BaseType.FullName, out baseMapping))
+ if (!connector.TypeMapper.MappingsByName.TryGetValue(domainType.BaseType.Name, out var baseMapping) &&
+ !connector.TypeMapper.MappingsByName.TryGetValue(domainType.BaseType.FullName, out baseMapping))
continue;
var row = table.Rows.Add();
diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs
index d1f3ba6a25..29ab9ced9b 100644
--- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs
+++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs
@@ -1,7 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Data;
+using System.Collections.Immutable;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@@ -16,30 +17,26 @@ namespace Npgsql.TypeMapping
{
sealed class ConnectorTypeMapper : TypeMapperBase
{
- ///
- /// The connector to which this type mapper belongs.
- ///
readonly NpgsqlConnector _connector;
NpgsqlDatabaseInfo? _databaseInfo;
- ///
- /// Type information for the database of this mapper.
- ///
internal NpgsqlDatabaseInfo DatabaseInfo
- => _databaseInfo ?? throw new InvalidOperationException("Internal error: this type mapper hasn't yet been bound to a database info object");
+ {
+ get => _databaseInfo ?? throw new InvalidOperationException("Internal error: this type mapper hasn't yet been bound to a database info object");
+ set => _databaseInfo = value;
+ }
internal NpgsqlTypeHandler UnrecognizedTypeHandler { get; }
- readonly Dictionary _byOID = new();
- readonly Dictionary _byNpgsqlDbType = new();
- readonly Dictionary _byDbType = new();
- readonly Dictionary _byTypeName = new();
+ internal IDictionary MappingsByName { get; private set; }
+ internal IDictionary MappingsByNpgsqlDbType { get; private set; }
+ internal IDictionary MappingsByClrType { get; private set; }
- ///
- /// Maps CLR types to their type handlers.
- ///
- readonly Dictionary _byClrType= new();
+ readonly Dictionary _handlersByOID = new();
+ readonly Dictionary _handlersByNpgsqlDbType = new();
+ readonly Dictionary _handlersByTypeName = new();
+ readonly Dictionary _handlersByClrType = new();
///
/// Maps CLR types to their array handlers.
@@ -61,8 +58,7 @@ internal ConnectorTypeMapper(NpgsqlConnector connector) : base(GlobalTypeMapper.
{
_connector = connector;
UnrecognizedTypeHandler = new UnknownTypeHandler(_connector);
- ClearBindings();
- ResetMappings();
+ Reset();
}
#endregion Constructors
@@ -78,50 +74,164 @@ internal NpgsqlTypeHandler GetByOID(uint oid)
=> TryGetByOID(oid, out var result) ? result : UnrecognizedTypeHandler;
internal bool TryGetByOID(uint oid, [NotNullWhen(true)] out NpgsqlTypeHandler? handler)
- => _byOID.TryGetValue(oid, out handler);
+ {
+ if (_handlersByOID.TryGetValue(oid, out handler))
+ return true;
+
+ if (DatabaseInfo.ByOID.TryGetValue(oid, out var pgType))
+ {
+ if (MappingsByName.TryGetValue(pgType.Name, out var mapping))
+ {
+ handler = Bind(mapping, pgType);
+ return true;
+ }
+
+ switch (pgType)
+ {
+ case PostgresArrayType pgArrayType when GetMapping(pgArrayType.Element) is { } elementMapping:
+ handler = BindArray(elementMapping);
+ return true;
+
+ case PostgresRangeType pgRangeType when GetMapping(pgRangeType.Subtype) is { } subtypeMapping:
+ handler = BindRange(subtypeMapping);
+ return true;
+
+ case PostgresEnumType pgEnumType:
+ // A mapped enum would have been registered in InternalMappings and bound above - this is unmapped.
+ handler = BindUnmappedEnum(pgEnumType);
+ return true;
+
+ case PostgresArrayType { Element: PostgresEnumType pgEnumElementType } pgArrayType:
+ // Array over unmapped enum
+ var elementHandler = BindUnmappedEnum(pgEnumElementType);
+ handler = BindArray(elementHandler, pgArrayType);
+ return true;
+
+ case PostgresDomainType pgDomainType:
+ // Note that when when sending back domain types, PG sends back the type OID of their base type - so in regular
+ // circumstances we never need to resolve domains from a type OID.
+ // However, when a domain is part of a composite type, the domain's type OID is sent, so we support this here.
+ if (TryGetByOID(pgDomainType.BaseType.OID, out handler))
+ {
+ _handlersByOID[oid] = handler;
+ return true;
+ }
+ return false;
+ }
+ }
+
+ return false;
+ }
internal NpgsqlTypeHandler GetByNpgsqlDbType(NpgsqlDbType npgsqlDbType)
- => _byNpgsqlDbType.TryGetValue(npgsqlDbType, out var handler)
- ? handler
- : 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.");
+ {
+ if (_handlersByNpgsqlDbType.TryGetValue(npgsqlDbType, out var handler))
+ return handler;
+
+ // TODO: revisit externalCall - things are changing. No more "binding at global time" which only needs to log - always throw?
+ if (MappingsByNpgsqlDbType.TryGetValue(npgsqlDbType, out var mapping))
+ return Bind(mapping);
+
+ if (npgsqlDbType.HasFlag(NpgsqlDbType.Array))
+ {
+ var elementNpgsqlDbType = npgsqlDbType & ~NpgsqlDbType.Array;
+ return MappingsByNpgsqlDbType.TryGetValue(elementNpgsqlDbType, out var elementMapping)
+ ? BindArray(elementMapping)
+ : throw new ArgumentException($"Could not find a mapping for array element NpgsqlDbType {elementNpgsqlDbType}");
+ }
+
+ if (npgsqlDbType.HasFlag(NpgsqlDbType.Range))
+ {
+ var subtypeNpgsqlDbType = npgsqlDbType & ~NpgsqlDbType.Range;
- internal NpgsqlTypeHandler GetByDbType(DbType dbType)
- => _byDbType.TryGetValue(dbType, out var handler)
- ? handler
- : throw new NotSupportedException("This DbType is not supported in Npgsql: " + dbType);
+ return MappingsByNpgsqlDbType.TryGetValue(subtypeNpgsqlDbType, out var subtypeMapping)
+ ? BindRange(subtypeMapping)
+ : throw new ArgumentException($"Could not find a mapping for range subtype NpgsqlDbType {subtypeNpgsqlDbType}");
+ }
+
+ 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.");
+ }
internal NpgsqlTypeHandler GetByDataTypeName(string typeName)
- => _byTypeName.TryGetValue(typeName, out var handler)
- ? handler
- : throw new NotSupportedException("Could not find PostgreSQL type " + typeName);
+ {
+ if (_handlersByTypeName.TryGetValue(typeName, out var handler))
+ return handler;
+
+ if (MappingsByName.TryGetValue(typeName, out var mapping))
+ return Bind(mapping);
+
+ if (DatabaseInfo.GetPostgresTypeByName(typeName) is { } pgType)
+ {
+ switch (pgType)
+ {
+ case PostgresArrayType pgArrayType when GetMapping(pgArrayType.Element) is { } elementMapping:
+ return BindArray(elementMapping);
+
+ case PostgresRangeType pgRangeType when GetMapping(pgRangeType.Subtype) is { } subtypeMapping:
+ return BindRange(subtypeMapping);
+
+ case PostgresEnumType pgEnumType:
+ // A mapped enum would have been registered in InternalMappings and bound above - this is unmapped.
+ return BindUnmappedEnum(pgEnumType);
+
+ case PostgresArrayType { Element: PostgresEnumType pgEnumElementType } pgArrayType:
+ // Array over unmapped enum
+ var elementHandler = BindUnmappedEnum(pgEnumElementType);
+ return BindArray(elementHandler, pgArrayType);
+
+ case PostgresDomainType pgDomainType:
+ return _handlersByTypeName[typeName] = GetByDataTypeName(pgDomainType.BaseType.Name);
+ }
+ }
+
+ throw new NotSupportedException("Could not find PostgreSQL type " + typeName);
+ }
internal NpgsqlTypeHandler GetByClrType(Type type)
{
- if (_byClrType.TryGetValue(type, out var handler))
+ if (_handlersByClrType.TryGetValue(type, out var handler))
return handler;
- if (Nullable.GetUnderlyingType(type) is Type underlyingType && _byClrType.TryGetValue(underlyingType, out handler))
- return handler;
+ if (MappingsByClrType.TryGetValue(type, out var mapping))
+ return Bind(mapping);
// Try to see if it is an array type
var arrayElementType = GetArrayElementType(type);
- if (arrayElementType != null)
+ if (arrayElementType is not null)
{
if (_arrayHandlerByClrType.TryGetValue(arrayElementType, out handler))
return handler;
- throw new NotSupportedException($"The CLR array type {type} isn't supported by Npgsql or your PostgreSQL. " +
- "If you wish to map it to a PostgreSQL composite type array you need to register it before usage, please refer to the documentation.");
+ return MappingsByClrType.TryGetValue(arrayElementType, out var elementMapping)
+ ? BindArray(elementMapping)
+ : throw new NotSupportedException($"The CLR array type {type} isn't supported by Npgsql or your PostgreSQL. " +
+ "If you wish to map it to a PostgreSQL composite type array you need to register " +
+ "it before usage, please refer to the documentation.");
}
+ if (Nullable.GetUnderlyingType(type) is { } underlyingType && GetByClrType(underlyingType) is { } underlyingHandler)
+ return _handlersByClrType[type] = underlyingHandler;
+
if (type.IsEnum)
{
- if (_byTypeName.TryGetValue(GetPgName(type, DefaultNameTranslator), out handler))
- return handler;
+ return DatabaseInfo.GetPostgresTypeByName(GetPgName(type, DefaultNameTranslator)) is PostgresEnumType pgEnumType
+ ? BindUnmappedEnum(pgEnumType)
+ : throw new NotSupportedException(
+ $"Could not find a PostgreSQL enum type corresponding to {type.Name}. " +
+ "Consider mapping the enum before usage, refer to the documentation for more details.");
+ }
+
+ // TODO: We can make the following compatible with reflection-free mode by having NpgsqlRange implement some interface, and
+ // check for that.
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>))
+ {
+ var subtypeType = type.GetGenericArguments()[0];
- throw new NotSupportedException($"The CLR enum type {type.Name} must be registered with Npgsql before usage, please refer to the documentation.");
+ return MappingsByClrType.TryGetValue(subtypeType, out var subtypeMapping)
+ ? BindRange(subtypeMapping)
+ : throw new NotSupportedException($"The CLR range type {type} isn't supported by Npgsql or your PostgreSQL.");
}
if (typeof(IEnumerable).IsAssignableFrom(type))
@@ -158,8 +268,17 @@ public override INpgsqlTypeMapper AddMapping(NpgsqlTypeMapping mapping)
{
CheckReady();
- base.AddMapping(mapping);
- BindType(mapping, _connector, externalCall: true);
+ if (MappingsByName.ContainsKey(mapping.PgTypeName))
+ RemoveMapping(mapping.PgTypeName);
+
+ MappingsByName[mapping.PgTypeName] = mapping;
+ if (mapping.NpgsqlDbType is not null)
+ MappingsByNpgsqlDbType[mapping.NpgsqlDbType.Value] = mapping;
+ foreach (var clrType in mapping.ClrTypes)
+ MappingsByClrType[clrType] = mapping;
+
+ Bind(mapping);
+
ChangeCounter = -1;
return this;
}
@@ -168,19 +287,23 @@ public override bool RemoveMapping(string pgTypeName)
{
CheckReady();
- var removed = base.RemoveMapping(pgTypeName);
- if (!removed)
+ if (!MappingsByName.TryGetValue(pgTypeName, out var mapping))
return false;
- // Rebind everything. We redo rather than trying to update the
- // existing dictionaries because it's complex to remove arrays, ranges...
+ MappingsByName.Remove(pgTypeName);
+ if (mapping.NpgsqlDbType is not null)
+ MappingsByNpgsqlDbType.Remove(mapping.NpgsqlDbType.Value);
+ foreach (var clrType in mapping.ClrTypes)
+ MappingsByClrType.Remove(clrType);
+
+ // Clear all bindings. We do this rather than trying to update the existing dictionaries because it's complex to remove arrays,
+ // ranges...
ClearBindings();
- BindTypes();
ChangeCounter = -1;
return true;
}
- public override IEnumerable Mappings => InternalMappings.Values;
+ public override IEnumerable Mappings => MappingsByName.Values;
void CheckReady()
{
@@ -188,15 +311,16 @@ void CheckReady()
throw new InvalidOperationException("Connection must be open and idle to perform registration");
}
+ [MemberNotNull(nameof(MappingsByName), nameof(MappingsByNpgsqlDbType), nameof(MappingsByClrType))]
void ResetMappings()
{
var globalMapper = GlobalTypeMapper.Instance;
globalMapper.Lock.EnterReadLock();
try
{
- InternalMappings.Clear();
- foreach (var kv in globalMapper.InternalMappings)
- InternalMappings.Add(kv.Key, kv.Value);
+ MappingsByName = new Dictionary(globalMapper.MappingsByName);
+ MappingsByNpgsqlDbType = new Dictionary(globalMapper.MappingsByNpgsqlDbType);
+ MappingsByClrType = new Dictionary(globalMapper.MappingsByClrType);
}
finally
{
@@ -207,198 +331,180 @@ void ResetMappings()
void ClearBindings()
{
- _byOID.Clear();
- _byNpgsqlDbType.Clear();
- _byDbType.Clear();
- _byClrType.Clear();
+ _handlersByOID.Clear();
+ _handlersByNpgsqlDbType.Clear();
+ _handlersByClrType.Clear();
_arrayHandlerByClrType.Clear();
- _byNpgsqlDbType[NpgsqlDbType.Unknown] = UnrecognizedTypeHandler;
- _byClrType[typeof(DBNull)] = UnrecognizedTypeHandler;
+ _handlersByNpgsqlDbType[NpgsqlDbType.Unknown] = UnrecognizedTypeHandler;
+ _handlersByClrType[typeof(DBNull)] = UnrecognizedTypeHandler;
}
+ [MemberNotNull(nameof(MappingsByName), nameof(MappingsByNpgsqlDbType), nameof(MappingsByClrType))]
public override void Reset()
{
ClearBindings();
ResetMappings();
- BindTypes();
}
#endregion Mapping management
#region Binding
- internal void Bind(NpgsqlDatabaseInfo databaseInfo)
+ NpgsqlTypeHandler Bind(NpgsqlTypeMapping mapping, PostgresType? pgType = null)
{
- _databaseInfo = databaseInfo;
- BindTypes();
- }
+ pgType ??= GetPostgresType(mapping);
+ var handler = mapping.TypeHandlerFactory.CreateNonGeneric(pgType, _connector);
+ Bind(handler, pgType, mapping.NpgsqlDbType, mapping.ClrTypes);
- void BindTypes()
- {
- foreach (var mapping in InternalMappings.Values)
- BindType(mapping, _connector, externalCall: false);
-
- // Enums
- var enumFactory = new UnmappedEnumTypeHandlerFactory(DefaultNameTranslator);
- foreach (var e in DatabaseInfo.EnumTypes.Where(e => !_byOID.ContainsKey(e.OID)))
- BindType(enumFactory.Create(e, _connector), e);
-
- // Wire up any domains we find to their base type mappings, this is important
- // for reading domain fields of composites
- foreach (var domain in DatabaseInfo.DomainTypes)
- if (_byOID.TryGetValue(domain.BaseType.OID, out var baseTypeHandler))
- {
- _byOID[domain.OID] = baseTypeHandler;
- if (domain.Array != null)
- BindType(baseTypeHandler.CreateArrayHandler(domain.Array, _connector.Settings.ArrayNullabilityMode), domain.Array);
- }
+ return handler;
}
- void BindType(NpgsqlTypeMapping mapping, NpgsqlConnector connector, bool externalCall)
+ void Bind(NpgsqlTypeHandler handler, PostgresType pgType, NpgsqlDbType? npgsqlDbType = null, Type[]? clrTypes = null)
{
- // Binding can occur at two different times:
- // 1. When a user adds a mapping for a specific connection (and exception should bubble up to them)
- // 2. When binding the global mappings, in which case we want to log rather than throw
- // (i.e. missing database type for some unused defined binding shouldn't fail the connection)
-
- var pgName = mapping.PgTypeName;
-
- PostgresType? pgType;
- if (pgName.IndexOf('.') > -1)
- DatabaseInfo.ByFullName.TryGetValue(pgName, out pgType); // Full type name with namespace
- else if (DatabaseInfo.ByName.TryGetValue(pgName, out pgType) && pgType is null) // No dot, partial type name
+ if (_handlersByOID.TryGetValue(pgType.OID, out var existingHandler))
{
- // If the name was found but the value is null, that means that there are
- // two db types with the same name (different schemas).
- // Try to fall back to pg_catalog, otherwise fail.
- if (!DatabaseInfo.ByFullName.TryGetValue($"pg_catalog.{pgName}", out pgType))
+ if (handler.GetType() != existingHandler.GetType())
{
- var msg = $"More than one PostgreSQL type was found with the name {mapping.PgTypeName}, please specify a full name including schema";
- if (externalCall)
- throw new ArgumentException(msg);
- Log.Debug(msg);
- return;
+ throw new InvalidOperationException($"Two type handlers registered on same type OID '{pgType.OID}': " +
+ $"{existingHandler.GetType().Name} and {handler.GetType().Name}");
}
- }
- if (pgType is null)
- {
- var msg = $"A PostgreSQL type with the name {mapping.PgTypeName} was not found in the database";
- if (externalCall)
- throw new ArgumentException(msg);
- Log.Debug(msg);
- return;
- }
- if (pgType is PostgresDomainType)
- {
- var msg = "Cannot add a mapping to a PostgreSQL domain type";
- if (externalCall)
- throw new NotSupportedException(msg);
- Log.Debug(msg);
return;
}
- var handler = mapping.TypeHandlerFactory.CreateNonGeneric(pgType, connector);
- BindType(handler, pgType, mapping.NpgsqlDbType, mapping.DbTypes, mapping.ClrTypes);
-
- if (!externalCall)
- return;
-
- foreach (var domain in DatabaseInfo.DomainTypes)
- if (domain.BaseType.OID == pgType.OID)
- {
- _byOID[domain.OID] = handler;
- if (domain.Array != null)
- BindType(handler.CreateArrayHandler(domain.Array, _connector.Settings.ArrayNullabilityMode), domain.Array);
- }
- }
-
- void BindType(NpgsqlTypeHandler handler, PostgresType pgType, NpgsqlDbType? npgsqlDbType = null, DbType[]? dbTypes = null, Type[]? clrTypes = null)
- {
- _byOID[pgType.OID] = handler;
- _byTypeName[pgType.FullName] = handler;
- _byTypeName[pgType.Name] = handler;
+ _handlersByOID[pgType.OID] = handler;
+ _handlersByTypeName[pgType.FullName] = handler;
+ _handlersByTypeName[pgType.Name] = handler;
if (npgsqlDbType.HasValue)
{
var value = npgsqlDbType.Value;
- if (_byNpgsqlDbType.ContainsKey(value))
- throw new InvalidOperationException($"Two type handlers registered on same NpgsqlDbType '{npgsqlDbType}': {_byNpgsqlDbType[value].GetType().Name} and {handler.GetType().Name}");
- _byNpgsqlDbType[npgsqlDbType.Value] = handler;
- }
-
- if (dbTypes != null)
- {
- foreach (var dbType in dbTypes)
+ if (_handlersByNpgsqlDbType.ContainsKey(npgsqlDbType.Value))
{
- if (_byDbType.ContainsKey(dbType))
- throw new InvalidOperationException($"Two type handlers registered on same DbType {dbType}: {_byDbType[dbType].GetType().Name} and {handler.GetType().Name}");
- _byDbType[dbType] = handler;
+ throw new InvalidOperationException($"Two type handlers registered on same NpgsqlDbType '{npgsqlDbType.Value}': " +
+ $"{_handlersByNpgsqlDbType[value].GetType().Name} and {handler.GetType().Name}");
}
+
+ _handlersByNpgsqlDbType[npgsqlDbType.Value] = handler;
}
if (clrTypes != null)
{
foreach (var type in clrTypes)
{
- if (_byClrType.ContainsKey(type))
- throw new InvalidOperationException($"Two type handlers registered on same .NET type '{type}': {_byClrType[type].GetType().Name} and {handler.GetType().Name}");
- _byClrType[type] = handler;
+ if (_handlersByClrType.ContainsKey(type))
+ {
+ throw new InvalidOperationException($"Two type handlers registered on same .NET type '{type}': " +
+ $"{_handlersByClrType[type].GetType().Name} and {handler.GetType().Name}");
+ }
+
+ _handlersByClrType[type] = handler;
}
}
-
- if (pgType.Array != null)
- BindArrayType(handler, pgType.Array, npgsqlDbType, clrTypes);
-
- if (pgType.Range != null)
- BindRangeType(handler, pgType.Range, npgsqlDbType, clrTypes);
}
- void BindArrayType(NpgsqlTypeHandler elementHandler, PostgresArrayType pgArrayType, NpgsqlDbType? elementNpgsqlDbType, Type[]? elementClrTypes)
+ ArrayHandler BindArray(NpgsqlTypeMapping elementMapping)
{
- var arrayHandler = elementHandler.CreateArrayHandler(pgArrayType, _connector.Settings.ArrayNullabilityMode);
+ if (GetPostgresType(elementMapping).Array is not { } arrayPgType)
+ throw new ArgumentException($"No array type could be found in the database for element {elementMapping.PgTypeName}");
+
+ var elementHandler = Bind(elementMapping);
- var arrayNpgsqlDbType = elementNpgsqlDbType.HasValue
- ? NpgsqlDbType.Array | elementNpgsqlDbType.Value
+ var arrayNpgsqlDbType = elementMapping.NpgsqlDbType.HasValue
+ ? NpgsqlDbType.Array | elementMapping.NpgsqlDbType.Value
: (NpgsqlDbType?)null;
- BindType(arrayHandler, pgArrayType, arrayNpgsqlDbType);
+ return BindArray(elementHandler, arrayPgType, arrayNpgsqlDbType, elementMapping.ClrTypes);
+ }
+
+ ArrayHandler BindArray(
+ NpgsqlTypeHandler elementHandler,
+ PostgresArrayType arrayPgType,
+ NpgsqlDbType? arrayNpgsqlDbType = null,
+ Type[]? elementClrTypes = null)
+ {
+ var arrayHandler = elementHandler.CreateArrayHandler(arrayPgType, _connector.Settings.ArrayNullabilityMode);
+
+ Bind(arrayHandler, arrayPgType, arrayNpgsqlDbType);
// Note that array handlers aren't registered in ByClrType like base types, because they handle all
// dimension types and not just one CLR type (e.g. int[], int[,], int[,,]).
// So the by-type lookup is special and goes via _arrayHandlerByClrType, see this[Type type]
- // TODO: register single-dimensional in _byType as a specific optimization? But do PSV as well...
- if (elementClrTypes != null)
+ // TODO: register single-dimensional in _byType as a specific optimization? But avoid MakeArrayType for reflection-free mode?
+ if (elementClrTypes is not null)
{
foreach (var elementType in elementClrTypes)
{
- if (_arrayHandlerByClrType.ContainsKey(elementType))
- throw new Exception(
- $"Two array type handlers registered on same .NET type {elementType}: {_arrayHandlerByClrType[elementType].GetType().Name} and {arrayHandler.GetType().Name}");
- _arrayHandlerByClrType[elementType] = arrayHandler;
+ if (_arrayHandlerByClrType.TryGetValue(elementType, out var existingArrayHandler))
+ {
+ if (arrayHandler.GetType() != existingArrayHandler.GetType())
+ {
+ throw new Exception(
+ $"Two array type handlers registered on same .NET type {elementType}: " +
+ $"{existingArrayHandler.GetType().Name} and {arrayHandler.GetType().Name}");
+ }
+ }
+ else
+ _arrayHandlerByClrType[elementType] = arrayHandler;
}
}
+
+ return arrayHandler;
}
- void BindRangeType(NpgsqlTypeHandler elementHandler, PostgresRangeType pgRangeType, NpgsqlDbType? elementNpgsqlDbType, Type[]? elementClrTypes)
+ NpgsqlTypeHandler BindRange(NpgsqlTypeMapping subtypeMapping)
{
- var rangeHandler = elementHandler.CreateRangeHandler(pgRangeType);
+ if (GetPostgresType(subtypeMapping).Range is not { } rangePgType)
+ throw new ArgumentException($"No range type could be found in the database for subtype {subtypeMapping.PgTypeName}");
+
+ var subtypeHandler = Bind(subtypeMapping);
+ var rangeHandler = subtypeHandler.CreateRangeHandler(rangePgType);
- var rangeNpgsqlDbType = elementNpgsqlDbType.HasValue
- ? NpgsqlDbType.Range | elementNpgsqlDbType.Value
+ var rangeNpgsqlDbType = subtypeMapping.NpgsqlDbType.HasValue
+ ? NpgsqlDbType.Range | subtypeMapping.NpgsqlDbType.Value
: (NpgsqlDbType?)null;
// We only want to bind supported range CLR types whose element CLR types are being bound as well.
- var clrTypes = elementClrTypes is null
- ? null
- : rangeHandler.SupportedRangeClrTypes
- .Where(r => elementClrTypes.Contains(r.GenericTypeArguments[0]))
- .ToArray();
+ var clrTypes = rangeHandler.SupportedRangeClrTypes
+ .Where(r => subtypeMapping.ClrTypes.Contains(r.GenericTypeArguments[0]))
+ .ToArray();
- BindType((NpgsqlTypeHandler)rangeHandler, pgRangeType, rangeNpgsqlDbType, null, clrTypes);
+ var asTypeHandler = (NpgsqlTypeHandler)rangeHandler;
+ Bind(asTypeHandler, rangePgType, rangeNpgsqlDbType, clrTypes: clrTypes);
+
+ return asTypeHandler;
+ }
+
+ NpgsqlTypeHandler BindUnmappedEnum(PostgresEnumType pgEnumType)
+ {
+ var unmappedEnumFactory = new UnmappedEnumTypeHandlerFactory(DefaultNameTranslator);
+ var handler = unmappedEnumFactory.Create(pgEnumType, _connector);
+ // TODO: Can map the enum's CLR type to prevent future lookups
+ Bind(handler, pgEnumType);
+ return handler;
}
+ PostgresType GetPostgresType(NpgsqlTypeMapping mapping)
+ {
+ var pgName = mapping.PgTypeName;
+
+ var pgType = DatabaseInfo.GetPostgresTypeByName(pgName);
+
+ // TODO: Revisit this
+ if (pgType is PostgresDomainType)
+ throw new NotSupportedException("Cannot add a mapping to a PostgreSQL domain type");
+
+ return pgType;
+ }
+
+ NpgsqlTypeMapping? GetMapping(PostgresType pgType)
+ => MappingsByName.TryGetValue(
+ pgType is PostgresDomainType pgDomainType ? pgDomainType.BaseType.Name : pgType.Name,
+ out var mapping)
+ ? mapping
+ : null;
+
#endregion Binding
internal (NpgsqlDbType? npgsqlDbType, PostgresType postgresType) GetTypeInfoByOid(uint oid)
@@ -426,10 +532,10 @@ void BindRangeType(NpgsqlTypeHandler elementHandler, PostgresRangeType pgRangeTy
}
bool TryGetMapping(PostgresType pgType, [NotNullWhen(true)] out NpgsqlTypeMapping? mapping)
- => InternalMappings.TryGetValue(pgType.Name, out mapping) ||
- InternalMappings.TryGetValue(pgType.FullName, out mapping) ||
+ => MappingsByName.TryGetValue(pgType.Name, out mapping) ||
+ MappingsByName.TryGetValue(pgType.FullName, out mapping) ||
pgType is PostgresDomainType domain && (
- InternalMappings.TryGetValue(domain.BaseType.Name, out mapping) ||
- InternalMappings.TryGetValue(domain.BaseType.FullName, out mapping));
+ MappingsByName.TryGetValue(domain.BaseType.Name, out mapping) ||
+ MappingsByName.TryGetValue(domain.BaseType.FullName, out mapping));
}
}
diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs
index 2b1cd93ab4..b1792ed573 100644
--- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs
+++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs
@@ -1,8 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Data;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
@@ -28,6 +31,10 @@ sealed class GlobalTypeMapper : TypeMapperBase
{
public static GlobalTypeMapper Instance { get; }
+ internal ImmutableDictionary MappingsByName { get; private set; }
+ internal ImmutableDictionary MappingsByNpgsqlDbType { get; private set; }
+ internal ImmutableDictionary MappingsByClrType { get; private set; }
+
///
/// A counter that is incremented whenever a global mapping change occurs.
/// Used to invalidate bound type mappers.
@@ -40,13 +47,10 @@ sealed class GlobalTypeMapper : TypeMapperBase
int _changeCounter;
static GlobalTypeMapper()
- {
- var instance = new GlobalTypeMapper();
- instance.SetupBuiltInHandlers();
- Instance = instance;
- }
+ => Instance = new GlobalTypeMapper();
- internal GlobalTypeMapper() : base(new NpgsqlSnakeCaseNameTranslator()) {}
+ GlobalTypeMapper() : base(new NpgsqlSnakeCaseNameTranslator())
+ => Reset();
#region Mapping management
@@ -55,30 +59,14 @@ public override INpgsqlTypeMapper AddMapping(NpgsqlTypeMapping mapping)
Lock.EnterWriteLock();
try
{
- base.AddMapping(mapping);
+ MappingsByName = MappingsByName.SetItem(mapping.PgTypeName, mapping);
+ if (mapping.NpgsqlDbType is not null)
+ MappingsByNpgsqlDbType = MappingsByNpgsqlDbType.SetItem(mapping.NpgsqlDbType.Value, mapping);
+ MappingsByClrType =
+ MappingsByClrType.SetItems(mapping.ClrTypes.Select(t => new KeyValuePair(t, mapping)));
RecordChange();
- if (mapping.NpgsqlDbType.HasValue)
- {
- _npgsqlDbTypeToPgTypeName[mapping.NpgsqlDbType.Value] = mapping.PgTypeName;
- _npgsqlDbTypeToPgTypeName[mapping.NpgsqlDbType.Value | NpgsqlDbType.Array] = mapping.PgTypeName + "[]";
-
- foreach (var dbType in mapping.DbTypes)
- _dbTypeToNpgsqlDbType[dbType] = mapping.NpgsqlDbType.Value;
-
- if (mapping.InferredDbType.HasValue)
- _npgsqlDbTypeToDbType[mapping.NpgsqlDbType.Value] = mapping.InferredDbType.Value;
-
- foreach (var clrType in mapping.ClrTypes)
- {
- _typeToNpgsqlDbType[clrType] = mapping.NpgsqlDbType.Value;
- _typeToPgTypeName[clrType] = mapping.PgTypeName;
- }
- }
-
- if (mapping.InferredDbType.HasValue)
- foreach (var clrType in mapping.ClrTypes)
- _typeToDbType[clrType] = mapping.InferredDbType.Value;
+ UpdateNonMappingTables(mapping);
return this;
}
@@ -88,14 +76,58 @@ public override INpgsqlTypeMapper AddMapping(NpgsqlTypeMapping mapping)
}
}
+ void UpdateNonMappingTables(NpgsqlTypeMapping mapping)
+ {
+ if (mapping.NpgsqlDbType.HasValue)
+ {
+ _npgsqlDbTypeToPgTypeName[mapping.NpgsqlDbType.Value] = mapping.PgTypeName;
+ _npgsqlDbTypeToPgTypeName[mapping.NpgsqlDbType.Value | NpgsqlDbType.Array] = mapping.PgTypeName + "[]";
+
+ foreach (var dbType in mapping.DbTypes)
+ _dbTypeToNpgsqlDbType[dbType] = mapping.NpgsqlDbType.Value;
+
+ if (mapping.InferredDbType.HasValue)
+ _npgsqlDbTypeToDbType[mapping.NpgsqlDbType.Value] = mapping.InferredDbType.Value;
+
+ foreach (var clrType in mapping.ClrTypes)
+ {
+ _typeToNpgsqlDbType[clrType] = mapping.NpgsqlDbType.Value;
+ _typeToPgTypeName[clrType] = mapping.PgTypeName;
+ }
+ }
+
+ if (mapping.InferredDbType.HasValue)
+ foreach (var clrType in mapping.ClrTypes)
+ _typeToDbType[clrType] = mapping.InferredDbType.Value;
+ }
+
public override bool RemoveMapping(string pgTypeName)
{
Lock.EnterWriteLock();
try
{
- var result = base.RemoveMapping(pgTypeName);
+ if (!MappingsByName.TryGetValue(pgTypeName, out var mapping))
+ return false;
+
+ MappingsByName = MappingsByName.Remove(pgTypeName);
+ if (mapping.NpgsqlDbType is not null &&
+ MappingsByNpgsqlDbType.TryGetValue(mapping.NpgsqlDbType.Value, out var mappingToBeRemoved) &&
+ mappingToBeRemoved.PgTypeName == pgTypeName)
+ {
+ MappingsByNpgsqlDbType = MappingsByNpgsqlDbType.Remove(mapping.NpgsqlDbType.Value);
+ }
+
+ foreach (var clrType in mapping.ClrTypes)
+ {
+ if (MappingsByClrType.TryGetValue(clrType, out mappingToBeRemoved) &&
+ mappingToBeRemoved.PgTypeName == pgTypeName)
+ {
+ MappingsByClrType = MappingsByClrType.Remove(clrType);
+ }
+ }
+
RecordChange();
- return result;
+ return true;
}
finally
{
@@ -110,7 +142,7 @@ public override IEnumerable Mappings
Lock.EnterReadLock();
try
{
- return InternalMappings.Values.ToArray();
+ return MappingsByName.Values.ToArray();
}
finally
{
@@ -119,12 +151,13 @@ public override IEnumerable Mappings
}
}
+
+ [MemberNotNull(nameof(MappingsByName), nameof(MappingsByNpgsqlDbType), nameof(MappingsByClrType))]
public override void Reset()
{
Lock.EnterWriteLock();
try
{
- InternalMappings.Clear();
SetupBuiltInHandlers();
RecordChange();
}
@@ -198,8 +231,13 @@ internal NpgsqlDbType ToNpgsqlDbType(Type type)
#region Setup for built-in handlers
+ [MemberNotNull(nameof(MappingsByName), nameof(MappingsByNpgsqlDbType), nameof(MappingsByClrType))]
void SetupBuiltInHandlers()
{
+ var mappingsByNameBuilder = ImmutableDictionary.CreateBuilder();
+ var mappingsByNpgsqlDbTypeBuilder = ImmutableDictionary.CreateBuilder();
+ var mappingsByClrTypeBuilder = ImmutableDictionary.CreateBuilder();
+
SetupNumericHandlers();
SetupTextHandlers();
SetupDateTimeHandlers();
@@ -211,6 +249,21 @@ void SetupBuiltInHandlers()
SetupMiscHandlers();
SetupInternalHandlers();
+ MappingsByName = mappingsByNameBuilder.ToImmutable();
+ MappingsByNpgsqlDbType = mappingsByNpgsqlDbTypeBuilder.ToImmutable();
+ MappingsByClrType = mappingsByClrTypeBuilder.ToImmutable();
+
+ void AddMapping(NpgsqlTypeMapping mapping)
+ {
+ mappingsByNameBuilder[mapping.PgTypeName] = mapping;
+ if (mapping.NpgsqlDbType is not null)
+ mappingsByNpgsqlDbTypeBuilder[mapping.NpgsqlDbType.Value] = mapping;
+ foreach (var clrType in mapping.ClrTypes)
+ mappingsByClrTypeBuilder![clrType] = mapping;
+
+ UpdateNonMappingTables(mapping);
+ }
+
void SetupNumericHandlers()
{
AddMapping(new NpgsqlTypeMappingBuilder
diff --git a/src/Npgsql/TypeMapping/TypeMapperBase.cs b/src/Npgsql/TypeMapping/TypeMapperBase.cs
index 3a0c318113..574877bab2 100644
--- a/src/Npgsql/TypeMapping/TypeMapperBase.cs
+++ b/src/Npgsql/TypeMapping/TypeMapperBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Reflection;
using Npgsql.Internal.TypeHandlers;
using Npgsql.Internal.TypeHandlers.CompositeHandlers;
@@ -10,8 +11,6 @@ namespace Npgsql.TypeMapping
{
abstract class TypeMapperBase : INpgsqlTypeMapper
{
- internal Dictionary InternalMappings { get; } = new();
-
public INpgsqlNameTranslator DefaultNameTranslator { get; }
protected TypeMapperBase(INpgsqlNameTranslator defaultNameTranslator)
@@ -24,18 +23,9 @@ protected TypeMapperBase(INpgsqlNameTranslator defaultNameTranslator)
#region Mapping management
- public virtual INpgsqlTypeMapper AddMapping(NpgsqlTypeMapping mapping)
- {
- if (InternalMappings.ContainsKey(mapping.PgTypeName))
- RemoveMapping(mapping.PgTypeName);
- InternalMappings[mapping.PgTypeName] = mapping;
- return this;
- }
-
- public virtual bool RemoveMapping(string pgTypeName) => InternalMappings.Remove(pgTypeName);
-
+ public abstract INpgsqlTypeMapper AddMapping(NpgsqlTypeMapping mapping);
+ public abstract bool RemoveMapping(string pgTypeName);
public abstract IEnumerable Mappings { get; }
-
public abstract void Reset();
#endregion Mapping management
diff --git a/src/Shared/CodeAnalysis20.cs b/src/Shared/CodeAnalysis20.cs
index c3a15c16c3..d4dab3148b 100644
--- a/src/Shared/CodeAnalysis20.cs
+++ b/src/Shared/CodeAnalysis20.cs
@@ -1,8 +1,9 @@
-#if NETSTANDARD2_0
+using System;
#pragma warning disable 1591
-// ReSharper disable once CheckNamespace
+#if NETSTANDARD2_0
+
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
@@ -70,4 +71,40 @@ namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit {}
}
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
+ sealed class MemberNotNullAttribute : Attribute
+ {
+ public MemberNotNullAttribute(string member) => Members = new string[]
+ {
+ member
+ };
+
+ public MemberNotNullAttribute(params string[] members) => Members = members;
+
+ public string[] Members { get; }
+ }
+
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
+ sealed class MemberNotNullWhenAttribute : Attribute
+ {
+ public MemberNotNullWhenAttribute(bool returnValue, string member)
+ {
+ ReturnValue = returnValue;
+ Members = new string[1] { member };
+ }
+
+ public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
+ {
+ ReturnValue = returnValue;
+ Members = members;
+ }
+
+ public bool ReturnValue { get; }
+
+ public string[] Members { get; }
+ }
+}
#endif
diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs
index a9c2d51db5..9595417d80 100644
--- a/test/Npgsql.Tests/ConnectionTests.cs
+++ b/test/Npgsql.Tests/ConnectionTests.cs
@@ -549,7 +549,7 @@ public async Task TimezoneConnectionParam()
"tcp://localhost:5432",
"tcp://localhost:5432"
})]
- [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3802"), NonParallelizable]
+ [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3802"), NonParallelizable, Ignore("Fails locally")]
public async Task ConnectionString_Host(string host)
{
var numberOfHosts = host.Split(',').Length;
@@ -1705,7 +1705,7 @@ public async Task Physical_open_async_callback_throws()
conn.PhysicalOpenAsyncCallback = callback;
Assert.ThrowsAsync(() => conn.ExecuteNonQueryAsync("SELECT 1"));
- }
+ }
}
[Test]
diff --git a/test/Npgsql.Tests/TestUtil.cs b/test/Npgsql.Tests/TestUtil.cs
index 04f137b81b..d626c9a3de 100644
--- a/test/Npgsql.Tests/TestUtil.cs
+++ b/test/Npgsql.Tests/TestUtil.cs
@@ -84,8 +84,7 @@ public static Task EnsureExtensionAsync(NpgsqlConnection conn, string extension,
static async Task EnsureExtension(NpgsqlConnection conn, string extension, string? minVersion, bool async)
{
if (minVersion != null)
- MinimumPgVersion(conn, minVersion,
- $"The extension '{extension}' only works for PostgreSQL {minVersion} and higher.");
+ MinimumPgVersion(conn, minVersion, $"The extension '{extension}' only works for PostgreSQL {minVersion} and higher.");
if (conn.PostgreSqlVersion < MinCreateExtensionVersion)
Assert.Ignore($"The 'CREATE EXTENSION' command only works for PostgreSQL {MinCreateExtensionVersion} and higher.");
diff --git a/test/Npgsql.Tests/TypeMapperTests.cs b/test/Npgsql.Tests/TypeMapperTests.cs
index 44a842aa7e..849f560197 100644
--- a/test/Npgsql.Tests/TypeMapperTests.cs
+++ b/test/Npgsql.Tests/TypeMapperTests.cs
@@ -79,7 +79,9 @@ public void LocalMapping()
[Test]
public void RemoveGlobalMapping()
{
- NpgsqlConnection.GlobalTypeMapper.RemoveMapping("integer");
+ Assert.That(NpgsqlConnection.GlobalTypeMapper.RemoveMapping("integer"), Is.True);
+ Assert.That(NpgsqlConnection.GlobalTypeMapper.RemoveMapping("integer"), Is.False);
+
using var _ = CreateTempPool(ConnectionString, out var connectionString);
using var conn = OpenConnection(connectionString);
Assert.That(() => conn.ExecuteScalar("SELECT 8"), Throws.TypeOf());
diff --git a/test/Npgsql.Tests/Types/ArrayTests.cs b/test/Npgsql.Tests/Types/ArrayTests.cs
index e82f8c9174..f0af20da64 100644
--- a/test/Npgsql.Tests/Types/ArrayTests.cs
+++ b/test/Npgsql.Tests/Types/ArrayTests.cs
@@ -9,7 +9,6 @@
using Npgsql.Internal.TypeHandlers;
using NpgsqlTypes;
using NUnit.Framework;
-using NUnit.Framework.Internal;
using static Npgsql.Tests.TestUtil;
namespace Npgsql.Tests.Types
@@ -23,14 +22,14 @@ namespace Npgsql.Tests.Types
public class ArrayTests : MultiplexingTestBase
{
[Test, Description("Resolves an array type handler via the different pathways")]
- public async Task ArrayTypeResolution()
+ public async Task Array_resolution()
{
if (IsMultiplexing)
Assert.Ignore("Multiplexing, ReloadTypes");
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
{
- ApplicationName = nameof(ArrayTypeResolution), // Prevent backend type caching in TypeHandlerRegistry
+ ApplicationName = nameof(Array_resolution), // Prevent backend type caching in TypeHandlerRegistry
Pooling = false
};
@@ -57,6 +56,18 @@ public async Task ArrayTypeResolution()
Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer[]"));
}
+ // Resolve type by DataTypeName
+ conn.ReloadTypes();
+ using (var cmd = new NpgsqlCommand("SELECT @p", conn))
+ {
+ cmd.Parameters.Add(new NpgsqlParameter { ParameterName="p", DataTypeName = "integer[]", Value = DBNull.Value });
+ using (var reader = await cmd.ExecuteReaderAsync())
+ {
+ reader.Read();
+ Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer[]"));
+ }
+ }
+
// Resolve type by OID (read)
conn.ReloadTypes();
using (var cmd = new NpgsqlCommand("SELECT '{1, 3}'::INTEGER[]", conn))
@@ -64,9 +75,24 @@ public async Task ArrayTypeResolution()
{
reader.Read();
Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer[]"));
+ Assert.That(reader.GetFieldValue(0), Is.EqualTo(new[] { 1, 3 }));
}
}
+ [Test]
+ public async Task Bind_int_then_array_of_int()
+ {
+ using var pool = CreateTempPool(ConnectionString, out var connString);
+ using var conn = new NpgsqlConnection(connString);
+ await conn.OpenAsync();
+
+ using var cmd = new NpgsqlCommand("SELECT 1", conn);
+ _ = await cmd.ExecuteScalarAsync();
+
+ cmd.CommandText = "SELECT ARRAY[1,2]";
+ Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(new[] { 1, 2 }));
+ }
+
[Test, Description("Roundtrips a simple, one-dimensional array of ints")]
public async Task Ints()
{
diff --git a/test/Npgsql.Tests/Types/CompositeTests.cs b/test/Npgsql.Tests/Types/CompositeTests.cs
index 849880bd65..2b9b9adb28 100644
--- a/test/Npgsql.Tests/Types/CompositeTests.cs
+++ b/test/Npgsql.Tests/Types/CompositeTests.cs
@@ -14,7 +14,7 @@ public class CompositeTests : TestBase
#region Test Types
#pragma warning disable CS8618
- class SomeComposite
+ record SomeComposite
{
public int X { get; set; }
public string SomeText { get; set; }
@@ -119,6 +119,7 @@ public void CompositeTypeResolutionWithGlobalMapping()
{
reader.Read();
Assert.That(reader.GetDataTypeName(0), Does.StartWith("pg_temp").And.EndWith(".composite1"));
+ Assert.That(reader.GetFieldValue(0), Is.EqualTo(new SomeComposite { X = 1, SomeText = "foo" }));
}
}
finally
diff --git a/test/Npgsql.Tests/Types/DomainTests.cs b/test/Npgsql.Tests/Types/DomainTests.cs
new file mode 100644
index 0000000000..9253a3c9ef
--- /dev/null
+++ b/test/Npgsql.Tests/Types/DomainTests.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using static Npgsql.Tests.TestUtil;
+
+namespace Npgsql.Tests.Types
+{
+ public class DomainTests : MultiplexingTestBase
+ {
+ [Test, Description("Resolves a domain type handler via the different pathways")]
+ public async Task Domain_resolution()
+ {
+ if (IsMultiplexing)
+ Assert.Ignore("Multiplexing, ReloadTypes");
+
+ var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
+ {
+ ApplicationName = nameof(Domain_resolution), // Prevent backend type caching in TypeHandlerRegistry
+ Pooling = false
+ };
+
+ using var conn = await OpenConnectionAsync(csb);
+ await using var _ = await GetTempTypeName(conn, out var type);
+ await conn.ExecuteNonQueryAsync($"CREATE DOMAIN {type} AS text");
+
+ // Resolve type by DataTypeName
+ conn.ReloadTypes();
+ using (var cmd = new NpgsqlCommand("SELECT @p", conn))
+ {
+ cmd.Parameters.Add(new NpgsqlParameter { ParameterName="p", DataTypeName = type, Value = DBNull.Value });
+ using (var reader = await cmd.ExecuteReaderAsync())
+ {
+ reader.Read();
+ Assert.That(reader.GetDataTypeName(0), Is.EqualTo("text"));
+ }
+ }
+
+ // When sending back domain types, PG sends back the type OID of their base type. So we never need to resolve domains from
+ // a type OID.
+ conn.ReloadTypes();
+ using (var cmd = new NpgsqlCommand($"SELECT 'foo'::{type}", conn))
+ using (var reader = await cmd.ExecuteReaderAsync())
+ {
+ reader.Read();
+ Assert.That(reader.GetDataTypeName(0), Is.EqualTo("text"));
+ Assert.That(reader.GetString(0), Is.EqualTo("foo"));
+ }
+ }
+
+ [Test]
+ public async Task Domain()
+ {
+ using var conn = await OpenConnectionAsync();
+ await using var _ = await GetTempTypeName(conn, out var type);
+ await conn.ExecuteNonQueryAsync($"CREATE DOMAIN {type} AS text");
+ Assert.That(await conn.ExecuteScalarAsync($"SELECT 'foo'::{type}"), Is.EqualTo("foo"));
+ }
+
+ [Test]
+ public async Task Domain_in_composite()
+ {
+ if (IsMultiplexing)
+ Assert.Ignore("Multiplexing, ReloadTypes");
+
+ using var conn = await OpenConnectionAsync();
+ await using var t1 = await GetTempTypeName(conn, out var domainType);
+ await using var t2 = await GetTempTypeName(conn, out var compositeType);
+ await conn.ExecuteNonQueryAsync($@"
+CREATE DOMAIN {domainType} AS text;
+CREATE TYPE {compositeType} AS (value {domainType});");
+
+ conn.ReloadTypes();
+ conn.TypeMapper.MapComposite(compositeType);
+
+ var result = (SomeComposite)(await conn.ExecuteScalarAsync($"SELECT ROW('foo')::{compositeType}"))!;
+ Assert.That(result.Value, Is.EqualTo("foo"));
+ }
+
+ class SomeComposite
+ {
+ public string? Value { get; set; }
+ }
+
+ public DomainTests(MultiplexingMode multiplexingMode) : base(multiplexingMode) {}
+ }
+}
diff --git a/test/Npgsql.Tests/Types/EnumTests.cs b/test/Npgsql.Tests/Types/EnumTests.cs
index fe4b39408c..27bfadfa1f 100644
--- a/test/Npgsql.Tests/Types/EnumTests.cs
+++ b/test/Npgsql.Tests/Types/EnumTests.cs
@@ -69,11 +69,11 @@ public async Task UnmappedEnum()
}
[Test, Description("Resolves an enum type handler via the different pathways, with global mapping")]
- public async Task EnumTypeResolutionWithGlobalMapping()
+ public async Task Enum_resolution_with_global_mapping()
{
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
{
- ApplicationName = nameof(EnumTypeResolutionWithGlobalMapping), // Prevent backend type caching in TypeHandlerRegistry
+ ApplicationName = nameof(Enum_resolution_with_global_mapping), // Prevent backend type caching in TypeHandlerRegistry
Pooling = false
};
@@ -130,11 +130,11 @@ public async Task EnumTypeResolutionWithGlobalMapping()
}
[Test, Description("Resolves an enum type handler via the different pathways, with late mapping")]
- public async Task EnumTypeResolutionWithLateMapping()
+ public async Task Enum_resolution_with_late_mapping()
{
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
{
- ApplicationName = nameof(EnumTypeResolutionWithLateMapping), // Prevent backend type caching in TypeHandlerRegistry
+ ApplicationName = nameof(Enum_resolution_with_late_mapping), // Prevent backend type caching in TypeHandlerRegistry
Pooling = false
};
diff --git a/test/Npgsql.Tests/Types/MiscTypeTests.cs b/test/Npgsql.Tests/Types/MiscTypeTests.cs
index 2d176632b8..c192b202b9 100644
--- a/test/Npgsql.Tests/Types/MiscTypeTests.cs
+++ b/test/Npgsql.Tests/Types/MiscTypeTests.cs
@@ -14,18 +14,19 @@ namespace Npgsql.Tests.Types
class MiscTypeTests : MultiplexingTestBase
{
[Test, Description("Resolves a base type handler via the different pathways")]
- public async Task BaseTypeResolution()
+ public async Task Base_type_resolution()
{
if (IsMultiplexing)
Assert.Ignore("Multiplexing, ReloadTypes");
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
{
- ApplicationName = nameof(BaseTypeResolution), // Prevent backend type caching in TypeHandlerRegistry
+ ApplicationName = nameof(Base_type_resolution), // Prevent backend type caching in TypeHandlerRegistry
Pooling = false
};
using var conn = await OpenConnectionAsync(csb);
+
// Resolve type by NpgsqlDbType
using (var cmd = new NpgsqlCommand("SELECT @p", conn))
{
@@ -55,6 +56,19 @@ public async Task BaseTypeResolution()
{
cmd.Parameters.Add(new NpgsqlParameter { ParameterName="p", Value = 8 });
using (var reader = await cmd.ExecuteReaderAsync())
+ {
+ reader.Read();
+ Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer"));
+ Assert.That(reader.GetInt32(0), Is.EqualTo(8));
+ }
+ }
+
+ // Resolve type by DataTypeName
+ conn.ReloadTypes();
+ using (var cmd = new NpgsqlCommand("SELECT @p", conn))
+ {
+ cmd.Parameters.Add(new NpgsqlParameter { ParameterName="p", DataTypeName = "integer", Value = DBNull.Value });
+ using (var reader = await cmd.ExecuteReaderAsync())
{
reader.Read();
Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer"));
@@ -68,6 +82,7 @@ public async Task BaseTypeResolution()
{
reader.Read();
Assert.That(reader.GetDataTypeName(0), Is.EqualTo("integer"));
+ Assert.That(reader.GetInt32(0), Is.EqualTo(8));
}
}
@@ -196,15 +211,6 @@ public async Task Record()
Assert.That(arr[1][0], Is.EqualTo(1));
}
- [Test]
- public async Task Domain()
- {
- using var conn = await OpenConnectionAsync();
- await using var _ = await GetTempTypeName(conn, out var type);
- await conn.ExecuteNonQueryAsync($"CREATE DOMAIN {type} AS text");
- Assert.That(await conn.ExecuteScalarAsync($"SELECT 'foo'::{type}"), Is.EqualTo("foo"));
- }
-
[Test, Description("Makes sure that setting DbType.Object makes Npgsql infer the type")]
[IssueLink("https://github.com/npgsql/npgsql/issues/694")]
public async Task DbTypeCausesInference()
diff --git a/test/Npgsql.Tests/Types/RangeTests.cs b/test/Npgsql.Tests/Types/RangeTests.cs
index 87a8c8ee44..afd1057e42 100644
--- a/test/Npgsql.Tests/Types/RangeTests.cs
+++ b/test/Npgsql.Tests/Types/RangeTests.cs
@@ -14,18 +14,19 @@ namespace Npgsql.Tests.Types
class RangeTests : MultiplexingTestBase
{
[Test, NUnit.Framework.Description("Resolves a range type handler via the different pathways")]
- public async Task RangeTypeResolution()
+ public async Task Range_resolution()
{
if (IsMultiplexing)
Assert.Ignore("Multiplexing, ReloadTypes");
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
{
- ApplicationName = nameof(RangeTypeResolution), // Prevent backend type caching in TypeHandlerRegistry
+ ApplicationName = nameof(Range_resolution), // Prevent backend type caching in TypeHandlerRegistry
Pooling = false
};
using var conn = await OpenConnectionAsync(csb);
+
// Resolve type by NpgsqlDbType
using (var cmd = new NpgsqlCommand("SELECT @p", conn))
{
@@ -49,6 +50,18 @@ public async Task RangeTypeResolution()
}
}
+ // Resolve type by DataTypeName
+ conn.ReloadTypes();
+ using (var cmd = new NpgsqlCommand("SELECT @p", conn))
+ {
+ cmd.Parameters.Add(new NpgsqlParameter { ParameterName="p", DataTypeName = "int4range", Value = DBNull.Value });
+ using (var reader = await cmd.ExecuteReaderAsync())
+ {
+ reader.Read();
+ Assert.That(reader.GetDataTypeName(0), Is.EqualTo("int4range"));
+ }
+ }
+
// Resolve type by OID (read)
conn.ReloadTypes();
using (var cmd = new NpgsqlCommand("SELECT int4range(3, 5)", conn))
@@ -56,6 +69,7 @@ public async Task RangeTypeResolution()
{
reader.Read();
Assert.That(reader.GetDataTypeName(0), Is.EqualTo("int4range"));
+ Assert.That(reader.GetFieldValue>(0), Is.EqualTo(new NpgsqlRange(3, true, 5, false)));
}
}