diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01e5549a1d..db805b8443 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -373,6 +373,9 @@ jobs: runs-on: ubuntu-24.04 if: github.event_name == 'push' && startsWith(github.repository, 'npgsql/') && needs.build.outputs.is_release == 'true' environment: nuget.org + permissions: + contents: read + id-token: write steps: - name: Checkout @@ -392,6 +395,12 @@ jobs: # TODO: Create a release + - name: NuGet login + uses: NuGet/login@v1 + id: nuget-login + with: + user: roji + - name: Publish to nuget.org - run: dotnet nuget push "*.nupkg" --api-key ${{ secrets.NUGET_ORG_API_KEY }} --source https://api.nuget.org/v3/index.json + run: dotnet nuget push "*.nupkg" --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json working-directory: nupkgs diff --git a/Directory.Build.props b/Directory.Build.props index 9ba57dbc8e..3939a65d83 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.2 + 10.0.3 latest true enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 52becfa8c3..1b5e908a5e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,7 +32,7 @@ - + @@ -61,8 +61,8 @@ - - + + diff --git a/src/Npgsql.Json.NET/Internal/JsonNetPocoTypeInfoResolverFactory.cs b/src/Npgsql.Json.NET/Internal/JsonNetPocoTypeInfoResolverFactory.cs index c038f17aab..8899eddb60 100644 --- a/src/Npgsql.Json.NET/Internal/JsonNetPocoTypeInfoResolverFactory.cs +++ b/src/Npgsql.Json.NET/Internal/JsonNetPocoTypeInfoResolverFactory.cs @@ -63,7 +63,9 @@ static void AddUserMappings(TypeInfoMappingCollection mappings, bool jsonb, Type || dataTypeName != JsonbDataTypeName && dataTypeName != JsonDataTypeName) return null; - return CreateCollection().AddMapping(type, dataTypeName, (options, mapping, _) => + var matchedType = Nullable.GetUnderlyingType(type) ?? type; + + return CreateCollection().AddMapping(matchedType, dataTypeName, (options, mapping, _) => { var jsonb = dataTypeName == JsonbDataTypeName; return mapping.CreateInfo(options, @@ -98,7 +100,12 @@ TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, TypeIn var dynamicMappings = CreateCollection(baseMappings); foreach (var mapping in baseMappings.Items) + { + // Always handle Nullable mappings as part of the underlying type. + if (Nullable.GetUnderlyingType(mapping.Type) is not null) + continue; dynamicMappings.AddArrayMapping(mapping.Type, mapping.DataTypeName); + } mappings.AddRange(dynamicMappings.ToTypeInfoMappingCollection()); return mappings; @@ -106,9 +113,8 @@ TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, TypeIn protected override DynamicMappingCollection? GetMappings(Type? type, DataTypeName dataTypeName, PgSerializerOptions options) => type is not null && IsArrayLikeType(type, out var elementType) && IsArrayDataTypeName(dataTypeName, options, out var elementDataTypeName) - ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(elementType, elementDataTypeName) + ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(Nullable.GetUnderlyingType(elementType) ?? elementType, elementDataTypeName) : null; } - } diff --git a/src/Npgsql/Internal/DynamicTypeInfoResolver.cs b/src/Npgsql/Internal/DynamicTypeInfoResolver.cs index 91af319207..d461c1fc1f 100644 --- a/src/Npgsql/Internal/DynamicTypeInfoResolver.cs +++ b/src/Npgsql/Internal/DynamicTypeInfoResolver.cs @@ -65,10 +65,11 @@ internal DynamicMappingCollection(TypeInfoMappingCollection? baseCollection = nu public DynamicMappingCollection AddMapping([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type type, string dataTypeName, TypeInfoFactory factory, Func? configureMapping = null) { - if (type.IsValueType && Nullable.GetUnderlyingType(type) is not null) - throw new NotSupportedException("Mapping nullable types is not supported, map its underlying type instead to get both."); - if (type.IsValueType) + { + if (Nullable.GetUnderlyingType(type) is not null) + throw new NotSupportedException("Mapping nullable types is not supported, map its underlying type instead to get both."); + typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddStructType), [typeof(string), typeof(TypeInfoFactory), typeof(Func)])! .MakeGenericMethod(type).Invoke(_mappings ??= new(), @@ -77,7 +78,9 @@ public DynamicMappingCollection AddMapping([DynamicallyAccessedMembers(Dynamical factory, configureMapping ]); + } else + { typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddType), [typeof(string), typeof(TypeInfoFactory), typeof(Func)])! .MakeGenericMethod(type).Invoke(_mappings ??= new(), @@ -86,28 +89,37 @@ public DynamicMappingCollection AddMapping([DynamicallyAccessedMembers(Dynamical factory, configureMapping ]); + } return this; } public DynamicMappingCollection AddArrayMapping([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type elementType, string dataTypeName) { if (elementType.IsValueType) + { + if (Nullable.GetUnderlyingType(elementType) is not null) + throw new NotSupportedException("Mapping nullable types is not supported, map its underlying type instead to get both."); + typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddStructArrayType), [typeof(string)])! .MakeGenericMethod(elementType).Invoke(_mappings ??= new(), [dataTypeName]); + } else + { typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddArrayType), [typeof(string)])! .MakeGenericMethod(elementType).Invoke(_mappings ??= new(), [dataTypeName]); + } return this; } public DynamicMappingCollection AddResolverMapping([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type type, string dataTypeName, TypeInfoFactory factory, Func? configureMapping = null) { - if (type.IsValueType && Nullable.GetUnderlyingType(type) is not null) - throw new NotSupportedException("Mapping nullable types is not supported"); - if (type.IsValueType) + { + if (Nullable.GetUnderlyingType(type) is not null) + throw new NotSupportedException("Mapping nullable types is not supported, map its underlying type instead to get both."); + typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddResolverStructType), [typeof(string), typeof(TypeInfoFactory), typeof(Func)])! .MakeGenericMethod(type).Invoke(_mappings ??= new(), @@ -116,7 +128,9 @@ public DynamicMappingCollection AddResolverMapping([DynamicallyAccessedMembers(D factory, configureMapping ]); + } else + { typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddResolverType), [typeof(string), typeof(TypeInfoFactory), typeof(Func)])! .MakeGenericMethod(type).Invoke(_mappings ??= new(), @@ -125,19 +139,27 @@ public DynamicMappingCollection AddResolverMapping([DynamicallyAccessedMembers(D factory, configureMapping ]); + } return this; } public DynamicMappingCollection AddResolverArrayMapping([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]Type elementType, string dataTypeName) { if (elementType.IsValueType) + { + if (Nullable.GetUnderlyingType(elementType) is not null) + throw new NotSupportedException("Mapping nullable types is not supported, map its underlying type instead to get both."); + typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddResolverStructArrayType), [typeof(string)])! .MakeGenericMethod(elementType).Invoke(_mappings ??= new(), [dataTypeName]); + } else + { typeof(TypeInfoMappingCollection) .GetMethod(nameof(TypeInfoMappingCollection.AddResolverArrayType), [typeof(string)])! .MakeGenericMethod(elementType).Invoke(_mappings ??= new(), [dataTypeName]); + } return this; } diff --git a/src/Npgsql/Internal/IntegratedSecurityHandler.cs b/src/Npgsql/Internal/IntegratedSecurityHandler.cs index 5edb826497..7589cc59e8 100644 --- a/src/Npgsql/Internal/IntegratedSecurityHandler.cs +++ b/src/Npgsql/Internal/IntegratedSecurityHandler.cs @@ -16,7 +16,7 @@ class IntegratedSecurityHandler return new(); } - public virtual ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector, CancellationToken cancellationToken) + public virtual ValueTask NegotiateAuthentication(bool async, bool isKerberos, NpgsqlConnector connector, CancellationToken cancellationToken) => throw new NotSupportedException(string.Format(NpgsqlStrings.IntegratedSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableIntegratedSecurity))); public virtual ValueTask GSSEncrypt(bool async, bool isRequired, NpgsqlConnector connector, CancellationToken cancellationToken) @@ -30,8 +30,8 @@ sealed class RealIntegratedSecurityHandler : IntegratedSecurityHandler public override ValueTask GetUsername(bool async, bool includeRealm, ILogger connectionLogger, CancellationToken cancellationToken) => KerberosUsernameProvider.GetUsername(async, includeRealm, connectionLogger, cancellationToken); - public override ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector, CancellationToken cancellationToken) - => connector.AuthenticateGSS(async, cancellationToken); + public override ValueTask NegotiateAuthentication(bool async, bool isKerberos, NpgsqlConnector connector, CancellationToken cancellationToken) + => connector.AuthenticateGSS(async, isKerberos, cancellationToken); public override ValueTask GSSEncrypt(bool async, bool isRequired, NpgsqlConnector connector, CancellationToken cancellationToken) => connector.GSSEncrypt(async, isRequired, cancellationToken); diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index f837f08026..1c2ef6c3cf 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -61,7 +61,8 @@ await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, username, as case AuthenticationRequestType.GSS: case AuthenticationRequestType.SSPI: ThrowIfNotAllowed(requiredAuthModes, msg.AuthRequestType == AuthenticationRequestType.GSS ? RequireAuthMode.GSS : RequireAuthMode.SSPI); - await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, this, cancellationToken).ConfigureAwait(false); + var isKerberos = msg.AuthRequestType == AuthenticationRequestType.GSS; + await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, isKerberos, this, cancellationToken).ConfigureAwait(false); return; case AuthenticationRequestType.GSSContinue: @@ -327,19 +328,34 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio await Flush(async, cancellationToken).ConfigureAwait(false); } - internal async ValueTask AuthenticateGSS(bool async, CancellationToken cancellationToken) + internal async ValueTask AuthenticateGSS(bool async, bool isKerberos, CancellationToken cancellationToken) { var targetName = $"{KerberosServiceName}/{Host}"; + // See https://github.com/postgres/postgres/blob/a0dd0702e464f206b08c99a74cb58809c51aafa5/src/interfaces/libpq/fe-auth.c#L111-L123 + // We do not support delegation (TokenImpersonationLevel.Delegation) for now (#6540) + var clientOptions = new NegotiateAuthenticationClientOptions + { + TargetName = targetName, + RequireMutualAuthentication = true + }; + // If postgres requests GSS, we explicitly ask for Kerberos + // Instead of relying on SSPI on windows to pick the correct protocol (Kerberos instead of NTLM) + // Otherwise, leave Negotiate to allow SSPI to pick whatever it thinks is correct + // This behavior differs from libpq, which prefers SSPI to pick the protocol + // But mimics PGJDBC + // On UNIX only Kerberos is supported, so no need to differentiate between OSes + // TODO: PGJBC has a parameter to force SSPI. Not sure we need something like this. + if (isKerberos) + clientOptions.Package = "Kerberos"; - var clientOptions = new NegotiateAuthenticationClientOptions { TargetName = targetName }; NegotiateOptionsCallback?.Invoke(clientOptions); using var authContext = new NegotiateAuthentication(clientOptions); var data = authContext.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; - if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) + if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) { // Unable to retrieve credentials or some other issue - throw new NpgsqlException($"Unable to authenticate with GSS: received {statusCode} instead of the expected ContinueNeeded"); + throw new NpgsqlException($"Unable to authenticate with GSS: received {statusCode} instead of the expected ContinueNeeded or Completed"); } await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false); await Flush(async, cancellationToken).ConfigureAwait(false); diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index bc4b2a1416..663577ea1a 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -583,6 +583,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca if (activity is not null) NpgsqlActivitySource.SetException(activity, e); Break(e, markHostAsOfflineOnConnecting: true); + FullCleanup(); throw; } @@ -672,7 +673,16 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ ConnectionLogger.LogTrace("Negotiating GSS encryption"); var targetName = $"{KerberosServiceName}/{Host}"; - var clientOptions = new NegotiateAuthenticationClientOptions { TargetName = targetName }; + // See https://github.com/postgres/postgres/blob/a0dd0702e464f206b08c99a74cb58809c51aafa5/src/interfaces/libpq/fe-secure-gssapi.c#L651-L658 + // We do not support delegation (TokenImpersonationLevel.Delegation) for now (#6540) + var clientOptions = new NegotiateAuthenticationClientOptions + { + TargetName = targetName, + RequireMutualAuthentication = true, + RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, + // GSS encryption only works with kerberos + Package = "Kerberos" + }; NegotiateOptionsCallback?.Invoke(clientOptions); @@ -696,7 +706,7 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ return GssEncryptionResult.GetCredentialFailure; } - if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) + if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) { // Unable to retrieve credentials // If it's required, throw an appropriate exception @@ -2498,7 +2508,7 @@ internal Exception Break(Exception reason, bool markHostAsOfflineOnConnecting = var connection = Connection; - FullCleanup(); + Cleanup(); if (connection is not null) { diff --git a/src/Npgsql/Internal/Postgres/DataTypeName.cs b/src/Npgsql/Internal/Postgres/DataTypeName.cs index 9c9f43e41a..8dd91b5508 100644 --- a/src/Npgsql/Internal/Postgres/DataTypeName.cs +++ b/src/Npgsql/Internal/Postgres/DataTypeName.cs @@ -52,13 +52,15 @@ public DataTypeName(string fullyQualifiedDataTypeName) internal static DataTypeName ValidatedName(string fullyQualifiedDataTypeName) => new(fullyQualifiedDataTypeName, validated: true); + bool IsUnqualifiedDisplayName => SchemaSpan is "pg_catalog" || IsUnqualified; + // Includes schema unless it's pg_catalog or the schema is an invalid character used to represent an unspecified schema. public string DisplayName => - Value.StartsWith("pg_catalog", StringComparison.Ordinal) || IsUnqualified + IsUnqualifiedDisplayName ? UnqualifiedDisplayName : Schema + "." + UnqualifiedDisplayName; - public string UnqualifiedDisplayName => ToDisplayName(UnqualifiedNameSpan); + public string UnqualifiedDisplayName => ToDisplayName(UnqualifiedNameSpan, mapAliases: IsUnqualifiedDisplayName); internal ReadOnlySpan SchemaSpan => Value.AsSpan(0, _value.IndexOf('.')); public string Schema => Value.Substring(0, _value.IndexOf('.')); @@ -124,27 +126,20 @@ public DataTypeName ToDefaultMultirangeName() // Create a DataTypeName from a broader range of valid names. // including SQL aliases like 'timestamp without time zone', trailing facet info etc. - public static DataTypeName FromDisplayName(string displayName, string? schema = null) - => FromDisplayName(displayName, schema, assumeUnqualified: false); // user strings may come fully qualified. - - // This method is used during type loading, it allows us to accept friendly names in constructors, without having to preconcatenate the schema. - internal static DataTypeName FromDisplayName(string displayName, string? schema, bool assumeUnqualified) + public static DataTypeName FromDisplayName(string displayName) { var displayNameSpan = displayName.AsSpan().Trim(); var schemaEndIndex = displayNameSpan.IndexOf('.'); ReadOnlySpan schemaSpan; - if (schemaEndIndex is not -1 && !assumeUnqualified) + if (schemaEndIndex is not -1) { - if (schema is not null) - throw new ArgumentException("Schema provided for a fully qualified name."); - schemaSpan = displayNameSpan.Slice(0, schemaEndIndex); displayNameSpan = displayNameSpan.Slice(schemaEndIndex + 1); } else { - schemaSpan = schema is null ? $"{InvalidIdentifier}" : schema.AsSpan(); + schemaSpan = $"{InvalidIdentifier}"; } // Then we strip either of the two valid array representations to get the base type name (with or without facets). @@ -196,7 +191,7 @@ internal static DataTypeName FromDisplayName(string displayName, string? schema, var value => value }; - if (schema is null && DataTypeNames.IsWellKnownUnqualifiedName(mapped)) + if (DataTypeNames.IsWellKnownUnqualifiedName(mapped)) schemaSpan = "pg_catalog".AsSpan(); return new(string.Concat(schemaSpan, ".", isArray ? "_" : "", mapped)); @@ -207,29 +202,33 @@ internal static DataTypeName FromDisplayName(string displayName, string? schema, // Additionally array types have a '_' prefix while for readability their element type should be postfixed with '[]'. // See the table for all the aliases https://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE // Alternatively some of the source lives at https://github.com/postgres/postgres/blob/c8e1ba736b2b9e8c98d37a5b77c4ed31baf94147/src/backend/utils/adt/format_type.c#L186 - static string ToDisplayName(ReadOnlySpan unqualifiedName) + static string ToDisplayName(ReadOnlySpan unqualifiedName, bool mapAliases) { var isArray = unqualifiedName.IndexOf('_') is 0; var baseTypeName = isArray ? unqualifiedName.Slice(1) : unqualifiedName; - var mappedBaseType = baseTypeName switch + string? mappedBaseType = null; + if (mapAliases) { - "bool" => "boolean", - "bpchar" => "character", - "decimal" => "numeric", - "float4" => "real", - "float8" => "double precision", - "int2" => "smallint", - "int4" => "integer", - "int8" => "bigint", - "time" => "time without time zone", - "timestamp" => "timestamp without time zone", - "timetz" => "time with time zone", - "timestamptz" => "timestamp with time zone", - "varbit" => "bit varying", - "varchar" => "character varying", - _ => null - }; + mappedBaseType = baseTypeName switch + { + "bool" => "boolean", + "bpchar" => "character", + "decimal" => "numeric", + "float4" => "real", + "float8" => "double precision", + "int2" => "smallint", + "int4" => "integer", + "int8" => "bigint", + "time" => "time without time zone", + "timestamp" => "timestamp without time zone", + "timetz" => "time with time zone", + "timestamptz" => "timestamp with time zone", + "varbit" => "bit varying", + "varchar" => "character varying", + _ => null + }; + } return isArray ? string.Concat(mappedBaseType ?? baseTypeName, "[]") diff --git a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs index 696aac8efb..8c53384772 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs @@ -106,7 +106,9 @@ void AddUserMappings(bool jsonb, Type[] clrTypes) || dataTypeName != DataTypeNames.Jsonb && dataTypeName != DataTypeNames.Json) return null; - return CreateCollection().AddMapping(type, dataTypeName, (options, mapping, _) => + var matchedType = Nullable.GetUnderlyingType(type) ?? type; + + return CreateCollection().AddMapping(matchedType, dataTypeName, (options, mapping, _) => { var jsonb = dataTypeName == DataTypeNames.Jsonb; @@ -141,7 +143,8 @@ sealed class ArrayResolver(Type[]? jsonbClrTypes = null, Type[]? jsonClrTypes = protected override DynamicMappingCollection? GetMappings(Type? type, DataTypeName dataTypeName, PgSerializerOptions options) => type is not null && IsArrayLikeType(type, out var elementType) && IsArrayDataTypeName(dataTypeName, options, out var elementDataTypeName) - ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(elementType, elementDataTypeName) + ? base.GetMappings(elementType, elementDataTypeName, options) + ?.AddArrayMapping(Nullable.GetUnderlyingType(elementType) ?? elementType, elementDataTypeName) : null; static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, TypeInfoMappingCollection baseMappings) @@ -151,7 +154,12 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, var dynamicMappings = CreateCollection(baseMappings); foreach (var mapping in baseMappings.Items) + { + // Always handle Nullable mappings as part of the underlying type. + if (Nullable.GetUnderlyingType(mapping.Type) is not null) + continue; dynamicMappings.AddArrayMapping(mapping.Type, mapping.DataTypeName); + } mappings.AddRange(dynamicMappings.ToTypeInfoMappingCollection()); return mappings; diff --git a/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs index f778bea186..6e926e49ef 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs @@ -79,6 +79,8 @@ sealed class BasicJsonTypeInfoResolver : IJsonTypeInfoResolver return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonArrayConverter); if (type == typeof(JsonValue)) return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonValueConverter); + if (type == typeof(JsonNode)) + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonNodeConverter); return null; } } @@ -101,6 +103,7 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mappings.AddArrayType(dataTypeName); mappings.AddArrayType(dataTypeName); mappings.AddArrayType(dataTypeName); + mappings.AddArrayType(dataTypeName); } return mappings; diff --git a/src/Npgsql/Internal/ResolverFactories/TupledRecordTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/TupledRecordTypeInfoResolverFactory.cs index 7ee00d37a7..551c2836b9 100644 --- a/src/Npgsql/Internal/ResolverFactories/TupledRecordTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/TupledRecordTypeInfoResolverFactory.cs @@ -19,12 +19,13 @@ class Resolver : DynamicTypeInfoResolver { protected override DynamicMappingCollection? GetMappings(Type? type, DataTypeName dataTypeName, PgSerializerOptions options) { - if (!(dataTypeName == DataTypeNames.Record && type is { IsConstructedGenericType: true, FullName: not null } && ( - type.FullName.StartsWith("System.Tuple", StringComparison.Ordinal) - || type.FullName.StartsWith("System.ValueTuple", StringComparison.Ordinal)))) + if (dataTypeName != DataTypeNames.Record || type is null || !IsTypeOrNullableOfType(type, + static type => type is { IsConstructedGenericType: true, FullName: not null } && + (type.FullName.StartsWith("System.Tuple", StringComparison.Ordinal) || + type.FullName.StartsWith("System.ValueTuple", StringComparison.Ordinal)), out var matchedType)) return null; - return CreateCollection().AddMapping(type, dataTypeName, (options, mapping, _) => + return CreateCollection().AddMapping(matchedType, dataTypeName, (options, mapping, _) => { var constructors = mapping.Type.GetConstructors(); ConstructorInfo? constructor = null; @@ -68,7 +69,7 @@ sealed class ArrayResolver : Resolver { protected override DynamicMappingCollection? GetMappings(Type? type, DataTypeName dataTypeName, PgSerializerOptions options) => type is not null && IsArrayLikeType(type, out var elementType) && IsArrayDataTypeName(dataTypeName, options, out var elementDataTypeName) - ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(elementType, elementDataTypeName) + ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(Nullable.GetUnderlyingType(elementType) ?? elementType, elementDataTypeName) : null; } } diff --git a/src/Npgsql/Internal/ResolverFactories/UnmappedTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/UnmappedTypeInfoResolverFactory.cs index d3dcabb467..bd3d93bdc2 100644 --- a/src/Npgsql/Internal/ResolverFactories/UnmappedTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/UnmappedTypeInfoResolverFactory.cs @@ -58,7 +58,7 @@ sealed class EnumArrayResolver : EnumResolver { protected override DynamicMappingCollection? GetMappings(Type? type, DataTypeName dataTypeName, PgSerializerOptions options) => type is not null && IsArrayLikeType(type, out var elementType) && IsArrayDataTypeName(dataTypeName, options, out var elementDataTypeName) - ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(elementType, elementDataTypeName) + ? base.GetMappings(elementType, elementDataTypeName, options)?.AddArrayMapping(Nullable.GetUnderlyingType(elementType) ?? elementType, elementDataTypeName) : null; } @@ -114,7 +114,12 @@ sealed class RangeArrayResolver : RangeResolver return null; var mappings = base.GetMappings(elementType, elementDataTypeName, options); + elementType ??= mappings?.Find(null, elementDataTypeName, options)?.Type; // Try to get the default mapping. + + if (elementType is not null && Nullable.GetUnderlyingType(elementType) is { } underlyingType) + elementType = underlyingType; + return elementType is null ? null : mappings?.AddArrayMapping(elementType, elementDataTypeName); } } @@ -168,7 +173,12 @@ sealed class MultirangeArrayResolver : MultirangeResolver return null; var mappings = base.GetMappings(elementType, elementDataTypeName, options); + elementType ??= mappings?.Find(null, elementDataTypeName, options)?.Type; // Try to get the default mapping. + + if (elementType is not null && Nullable.GetUnderlyingType(elementType) is { } underlyingType) + elementType = underlyingType; + return elementType is null ? null : mappings?.AddArrayMapping(elementType, elementDataTypeName); } } diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index 57299a3eec..5973efc92d 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -1925,7 +1925,14 @@ public void ReloadTypes() /// Flushes the type cache for this connection's connection string and reloads the types for this connection only. /// Type changes will appear for other connections only after they are re-opened from the pool. /// - public async Task ReloadTypesAsync(CancellationToken cancellationToken = default) + public Task ReloadTypesAsync() + => ReloadTypesAsync(CancellationToken.None); + + /// + /// Flushes the type cache for this connection's connection string and reloads the types for this connection only. + /// Type changes will appear for other connections only after they are re-opened from the pool. + /// + public async Task ReloadTypesAsync(CancellationToken cancellationToken) { CheckReady(); diff --git a/src/Npgsql/NpgsqlMultiHostDataSource.cs b/src/Npgsql/NpgsqlMultiHostDataSource.cs index 4ccc0809b5..d06fc3c982 100644 --- a/src/Npgsql/NpgsqlMultiHostDataSource.cs +++ b/src/Npgsql/NpgsqlMultiHostDataSource.cs @@ -279,6 +279,12 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType) return connector; } + catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken == cancellationToken) + { + if (connector is not null) + pool.Return(connector); + throw; + } catch (Exception ex) { exceptions.Add(ex); diff --git a/src/Npgsql/PostgresDatabaseInfo.cs b/src/Npgsql/PostgresDatabaseInfo.cs index 1c1b518a3f..6218b0a8d6 100644 --- a/src/Npgsql/PostgresDatabaseInfo.cs +++ b/src/Npgsql/PostgresDatabaseInfo.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Npgsql.BackendMessages; using Npgsql.Internal; +using Npgsql.Internal.Postgres; using Npgsql.PostgresTypes; using Npgsql.Util; using static Npgsql.Util.Statics; @@ -523,7 +524,7 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar switch (postgresTypeDefinition.Type) { case 'b': // Normal base type - var baseType = new PostgresBaseType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID); + var baseType = new PostgresBaseType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID); byOID[baseType.OID] = baseType; return true; @@ -537,7 +538,7 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar return false; } - var arrayType = new PostgresArrayType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID, elementPostgresType); + var arrayType = new PostgresArrayType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID, elementPostgresType); byOID[arrayType.OID] = arrayType; return true; } @@ -552,7 +553,7 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar return false; } - var rangeType = new PostgresRangeType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID, subtypePostgresType); + var rangeType = new PostgresRangeType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID, subtypePostgresType); byOID[rangeType.OID] = rangeType; return true; } @@ -573,17 +574,17 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar return false; } - var multirangeType = new PostgresMultirangeType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID, rangePostgresType); + var multirangeType = new PostgresMultirangeType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID, rangePostgresType); byOID[multirangeType.OID] = multirangeType; return true; case 'e': // Enum - var enumType = new PostgresEnumType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID); + var enumType = new PostgresEnumType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID); byOID[enumType.OID] = enumType; return true; case 'c': // Composite - var compositeType = new PostgresCompositeType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID); + var compositeType = new PostgresCompositeType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID); byOID[compositeType.OID] = compositeType; return true; @@ -596,7 +597,7 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar return false; } - var domainType = new PostgresDomainType(postgresTypeDefinition.Namespace, postgresTypeDefinition.Name, postgresTypeDefinition.OID, basePostgresType, postgresTypeDefinition.NotNull); + var domainType = new PostgresDomainType(postgresTypeDefinition.DataTypeName, postgresTypeDefinition.OID, basePostgresType, postgresTypeDefinition.NotNull); byOID[domainType.OID] = domainType; return true; @@ -610,4 +611,7 @@ bool TryAddPostgresType(PostgresTypeDefinition postgresTypeDefinition, Dictionar } } -readonly record struct PostgresTypeDefinition(string Namespace, uint OID, string Name, char Type, bool NotNull, uint ElemTypeOID); +readonly record struct PostgresTypeDefinition(string Namespace, uint OID, string Name, char Type, bool NotNull, uint ElemTypeOID) +{ + public DataTypeName DataTypeName => DataTypeName.CreateFullyQualifiedName(Namespace + "." + Name); +} diff --git a/src/Npgsql/PostgresTypes/PostgresType.cs b/src/Npgsql/PostgresTypes/PostgresType.cs index 842d1f3eea..fc88eb1304 100644 --- a/src/Npgsql/PostgresTypes/PostgresType.cs +++ b/src/Npgsql/PostgresTypes/PostgresType.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Npgsql.Internal.Postgres; namespace Npgsql.PostgresTypes; @@ -20,13 +21,12 @@ public abstract class PostgresType /// Constructs a representation of a PostgreSQL data type. /// /// The data type's namespace (or schema). - /// The data type's name. + /// The data type's display name. /// The data type's OID. private protected PostgresType(string ns, string name, uint oid) { - DataTypeName = DataTypeName.FromDisplayName(name, ns, assumeUnqualified: true); + DataTypeName = DataTypeName.FromDisplayName(ns is null or "pg_catalog" ? name : ns + "." + name); OID = oid; - FullName = Namespace + "." + Name; } /// @@ -38,7 +38,6 @@ private protected PostgresType(DataTypeName dataTypeName, Oid oid) { DataTypeName = dataTypeName; OID = oid.Value; - FullName = Namespace + "." + Name; } #endregion @@ -67,7 +66,8 @@ private protected PostgresType(DataTypeName dataTypeName, Oid oid) /// /// The full name of the backend type, including its namespace. /// - public string FullName { get; } + [field: MaybeNull] + public string FullName => field ??= Namespace + "." + Name; internal DataTypeName DataTypeName { get; } diff --git a/src/Npgsql/PublicAPI.Shipped.txt b/src/Npgsql/PublicAPI.Shipped.txt index 3ec604ddc0..84bb317e6f 100644 --- a/src/Npgsql/PublicAPI.Shipped.txt +++ b/src/Npgsql/PublicAPI.Shipped.txt @@ -402,6 +402,7 @@ Npgsql.NpgsqlConnection.ProvidePasswordCallback.get -> Npgsql.ProvidePasswordCal Npgsql.NpgsqlConnection.ProvidePasswordCallback.set -> void Npgsql.NpgsqlConnection.ReloadTypes() -> void Npgsql.NpgsqlConnection.ReloadTypesAsync() -> System.Threading.Tasks.Task! +Npgsql.NpgsqlConnection.ReloadTypesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Npgsql.NpgsqlConnection.Timezone.get -> string! Npgsql.NpgsqlConnection.TypeMapper.get -> Npgsql.TypeMapping.INpgsqlTypeMapper! Npgsql.NpgsqlConnection.UnprepareAll() -> void diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 6694c16f4f..3f09fddd12 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -94,8 +94,6 @@ override Npgsql.NpgsqlDataReader.GetColumnSchemaAsync(System.Threading.Cancellat override Npgsql.NpgsqlMultiHostDataSource.Clear() -> void Npgsql.NpgsqlDataSource.ReloadTypes() -> void Npgsql.NpgsqlDataSource.ReloadTypesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Npgsql.NpgsqlConnection.ReloadTypesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -*REMOVED*Npgsql.NpgsqlConnection.ReloadTypesAsync() -> System.Threading.Tasks.Task! *REMOVED*Npgsql.NpgsqlDataSourceBuilder.MapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlDataSourceBuilder.MapComposite(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs index ef3981d22f..4e2c13e69d 100644 --- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs +++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Threading; using Npgsql.Internal; using Npgsql.Internal.Postgres; using Npgsql.Internal.ResolverFactories; @@ -14,84 +13,64 @@ sealed class GlobalTypeMapper : INpgsqlTypeMapper { readonly UserTypeMapper _userTypeMapper = new(); readonly List _pluginResolverFactories = []; - readonly ReaderWriterLockSlim _lock = new(); + readonly object _sync = new(); PgTypeInfoResolverFactory[] _typeMappingResolvers = []; internal IEnumerable GetPluginResolverFactories() { - var resolvers = new List(); - _lock.EnterReadLock(); - try - { - resolvers.AddRange(_pluginResolverFactories); - } - finally - { - _lock.ExitReadLock(); - } - - return resolvers; + lock (_sync) + return new List(_pluginResolverFactories); } internal PgTypeInfoResolverFactory? GetUserMappingsResolverFactory() { - _lock.EnterReadLock(); - try - { + lock (_sync) return _userTypeMapper.Items.Count > 0 ? _userTypeMapper : null; - } - finally - { - _lock.ExitReadLock(); - } } internal void AddGlobalTypeMappingResolvers(PgTypeInfoResolverFactory[] factories, Func? builderFactory = null, bool overwrite = false) { - // Good enough logic to prevent SlimBuilder overriding the normal Builder. - if (overwrite || factories.Length > _typeMappingResolvers.Length) + lock (_sync) { - _builderFactory = builderFactory; - _typeMappingResolvers = factories; - ResetTypeMappingCache(); + // Good enough logic to prevent SlimBuilder overriding the normal Builder. + if (overwrite || factories.Length > _typeMappingResolvers.Length) + { + _builderFactory = builderFactory; + _typeMappingResolvers = factories; + _typeMappingOptions = null; + } } } - void ResetTypeMappingCache() => _typeMappingOptions = null; - PgSerializerOptions? _typeMappingOptions; Func? _builderFactory; JsonSerializerOptions? _jsonSerializerOptions; - PgSerializerOptions TypeMappingOptions + PgSerializerOptions TypeMappingOptions => _typeMappingOptions ?? BuildTypeMappingOptions(); + + PgSerializerOptions BuildTypeMappingOptions() { - get + lock (_sync) { - if (_typeMappingOptions is not null) - return _typeMappingOptions; - - _lock.EnterReadLock(); - try + if (_typeMappingOptions is { } existing) + return existing; + + var builder = _builderFactory?.Invoke() ?? new(); + builder.AppendResolverFactory(_userTypeMapper); + foreach (var factory in _pluginResolverFactories) + builder.AppendResolverFactory(factory); + foreach (var factory in _typeMappingResolvers) + builder.AppendResolverFactory(factory); + var chain = builder.Build(); + var options = new PgSerializerOptions(PostgresMinimalDatabaseInfo.DefaultTypeCatalog, chain) { - var builder = _builderFactory?.Invoke() ?? new(); - builder.AppendResolverFactory(_userTypeMapper); - foreach (var factory in _pluginResolverFactories) - builder.AppendResolverFactory(factory); - foreach (var factory in _typeMappingResolvers) - builder.AppendResolverFactory(factory); - var chain = builder.Build(); - return _typeMappingOptions = new(PostgresMinimalDatabaseInfo.DefaultTypeCatalog, chain) - { - // This means we don't ever have a missing oid for a datatypename as our canonical format is datatypenames. - PortableTypeIds = true, - // Don't throw if our catalog doesn't know the datatypename. - IntrospectionMode = true - }; - } - finally - { - _lock.ExitReadLock(); - } + // This means we don't ever have a missing oid for a datatypename as our canonical format is datatypenames. + PortableTypeIds = true, + // Don't throw if our catalog doesn't know the datatypename. + IntrospectionMode = true + }; + _typeMappingOptions = options; + return options; } } @@ -121,8 +100,7 @@ static GlobalTypeMapper() /// public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) { - _lock.EnterWriteLock(); - try + lock (_sync) { var type = factory.GetType(); @@ -140,53 +118,21 @@ public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) } _pluginResolverFactories.Insert(0, factory); - ResetTypeMappingCache(); - } - finally - { - _lock.ExitWriteLock(); + _typeMappingOptions = null; } } public void AddDbTypeResolverFactory(DbTypeResolverFactory factory) => throw new NotSupportedException("The global type mapper does not support DbTypeResolverFactories. Call this method on a data source builder instead."); - void ReplaceTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) - { - _lock.EnterWriteLock(); - try - { - var type = factory.GetType(); - - for (var i = 0; i < _pluginResolverFactories.Count; i++) - { - if (_pluginResolverFactories[i].GetType() == type) - { - _pluginResolverFactories[i] = factory; - break; - } - } - - ResetTypeMappingCache(); - } - finally - { - _lock.ExitWriteLock(); - } - } - /// public void Reset() { - _lock.EnterWriteLock(); - try + lock (_sync) { _pluginResolverFactories.Clear(); _userTypeMapper.Items.Clear(); - } - finally - { - _lock.ExitWriteLock(); + _typeMappingOptions = null; } } @@ -200,9 +146,25 @@ public INpgsqlNameTranslator DefaultNameTranslator /// public INpgsqlTypeMapper ConfigureJsonOptions(JsonSerializerOptions serializerOptions) { - _jsonSerializerOptions = serializerOptions; - // If JsonTypeInfoResolverFactory exists we replace it with a configured instance on the same index of the array. - ReplaceTypeInfoResolverFactory(new JsonTypeInfoResolverFactory(serializerOptions)); + lock (_sync) + { + _jsonSerializerOptions = serializerOptions; + + // If JsonTypeInfoResolverFactory exists we replace it with a configured instance on the same index of the array. + var factory = new JsonTypeInfoResolverFactory(serializerOptions); + var type = factory.GetType(); + + for (var i = 0; i < _pluginResolverFactories.Count; i++) + { + if (_pluginResolverFactories[i].GetType() == type) + { + _pluginResolverFactories[i] = factory; + break; + } + } + + _typeMappingOptions = null; + } return this; } @@ -213,7 +175,9 @@ public INpgsqlTypeMapper EnableDynamicJson( Type[]? jsonbClrTypes = null, Type[]? jsonClrTypes = null) { - AddTypeInfoResolverFactory(new JsonDynamicTypeInfoResolverFactory(jsonbClrTypes, jsonClrTypes, _jsonSerializerOptions)); + // Use a re-entered lock to add the read of _jsonSerializerOptions to the total scope. + lock (_sync) + AddTypeInfoResolverFactory(new JsonDynamicTypeInfoResolverFactory(jsonbClrTypes, jsonClrTypes, _jsonSerializerOptions)); return this; } @@ -238,33 +202,23 @@ public INpgsqlTypeMapper EnableUnmappedTypes() /// public INpgsqlTypeMapper MapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum { - _lock.EnterWriteLock(); - try + lock (_sync) { _userTypeMapper.MapEnum(pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return this; } - finally - { - _lock.ExitWriteLock(); - } } /// public bool UnmapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum { - _lock.EnterWriteLock(); - try + lock (_sync) { var removed = _userTypeMapper.UnmapEnum(pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return removed; } - finally - { - _lock.ExitWriteLock(); - } } /// @@ -272,34 +226,24 @@ public INpgsqlTypeMapper EnableUnmappedTypes() public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) { - _lock.EnterWriteLock(); - try + lock (_sync) { _userTypeMapper.MapEnum(clrType, pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return this; } - finally - { - _lock.ExitWriteLock(); - } } /// public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) { - _lock.EnterWriteLock(); - try + lock (_sync) { var removed = _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return removed; } - finally - { - _lock.ExitWriteLock(); - } } /// @@ -317,17 +261,12 @@ public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes public INpgsqlTypeMapper MapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) { - _lock.EnterWriteLock(); - try + lock (_sync) { _userTypeMapper.MapComposite(clrType, pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return this; } - finally - { - _lock.ExitWriteLock(); - } } /// @@ -335,16 +274,11 @@ public INpgsqlTypeMapper MapComposite([DynamicallyAccessedMembers(DynamicallyAcc public bool UnmapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) { - _lock.EnterWriteLock(); - try + lock (_sync) { var result = _userTypeMapper.UnmapComposite(clrType, pgName, nameTranslator); - ResetTypeMappingCache(); + _typeMappingOptions = null; return result; } - finally - { - _lock.ExitWriteLock(); - } } } diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index f776e3bc4b..8ab2624d39 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -1596,6 +1596,7 @@ public async Task Sync_open_blocked_same_thread() } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6427")] + [Platform(Include = "Win")] // Hangs on linux and mac (probably because of missing kerberos token) public async Task Gss_encryption_retry_does_not_clear_pool() { if (IsMultiplexing) @@ -1603,23 +1604,53 @@ public async Task Gss_encryption_retry_does_not_clear_pool() var csb = new NpgsqlConnectionStringBuilder(ConnectionString) { - GssEncryptionMode = GssEncryptionMode.Prefer + GssEncryptionMode = GssEncryptionMode.Prefer, + NoResetOnClose = false }; // Break connection on gss encryption request to force the client to create a new connection and retry again // This emulates the behavior of older versions of PostgreSQL or its forks, like Supabase await using var postmaster = PgPostmasterMock.Start(csb.ConnectionString, breakOnGssEncryptionRequest: true); - await using var dataSource = CreateDataSource(postmaster.ConnectionString); + await using var dataSource = CreateDataSource(builder => + { + builder.ConnectionStringBuilder.ConnectionString = postmaster.ConnectionString; + // We use kerberos by default, which requires specific credentials to work + // Change it negotiate so SSPI on windows can use NTLM credentials + builder.UseNegotiateOptionsCallback(options => options.Package = "Negotiate"); + }); + + PgServerMock server; int processID; await using (var conn = await dataSource.OpenConnectionAsync()) { processID = conn.ProcessID; + + // The next connection request isn't valid because it was retried + await postmaster.SkipNextConnection(); + + var queryTask = conn.ExecuteNonQueryAsync("SELECT 1"); + + server = await postmaster.WaitForServerConnection(); + await server.ExpectExtendedQuery(); + await server.WriteScalarResponseAndFlush(1); + await queryTask; } // The second time we get a connection from the pool we should ge the exact same connection await using (var conn = await dataSource.OpenConnectionAsync()) { Assert.That(conn.ProcessID, Is.EqualTo(processID)); + + var queryTask = conn.ExecuteNonQueryAsync("SELECT 1"); + + // We do not set NoResetOnClose=true on connection string to test query behavior after connection retry + await server.ExpectSimpleQuery("DISCARD ALL"); + await server.ExpectExtendedQuery(); + server + .WriteCommandComplete() + .WriteReadyForQuery(); + await server.WriteScalarResponseAndFlush(1); + await queryTask; } } diff --git a/test/Npgsql.Tests/DataTypeNameTests.cs b/test/Npgsql.Tests/DataTypeNameTests.cs index 067eb217c4..acd209060e 100644 --- a/test/Npgsql.Tests/DataTypeNameTests.cs +++ b/test/Npgsql.Tests/DataTypeNameTests.cs @@ -60,9 +60,27 @@ public string ToDefaultMultirangeNameHasRange(string name) [TestCase("name ", "public", ExpectedResult = "public.name")] [TestCase("_name", "public", ExpectedResult = "public._name")] [TestCase("name[]", "public", ExpectedResult = "public._name")] - [TestCase("timestamp with time zone", "public", ExpectedResult = "public.timestamptz")] - [TestCase("boolean(facet_name)", "public", ExpectedResult = "public.bool")] + [TestCase("timestamp with time zone", "public", ExpectedResult = "public.timestamp with time zone")] + [TestCase("timestamp with time zone", "pg_catalog", ExpectedResult = "pg_catalog.timestamptz")] + [TestCase("timestamp with time zone", null, ExpectedResult = "pg_catalog.timestamptz")] + [TestCase("boolean(facet_name)", "public", ExpectedResult = "public.boolean(facet_name)")] + [TestCase("boolean(facet_name)", "pg_catalog", ExpectedResult = "pg_catalog.bool")] + [TestCase("boolean(facet_name)", null, ExpectedResult = "pg_catalog.bool")] [TestCase(" public.name ", null, ExpectedResult = "public.name")] + [TestCase("decimal", "public", ExpectedResult = "public.decimal")] + [TestCase("numeric", "public", ExpectedResult = "public.numeric")] public string FromDisplayName(string name, string? schema) - => DataTypeName.FromDisplayName(name, schema).Value; + => DataTypeName.FromDisplayName(schema is null or "pg_catalog" ? name : schema + "." + name).Value; + + [TestCase("pg_catalog.bool", ExpectedResult = "boolean")] + [TestCase("public.bool", ExpectedResult = "bool")] + [TestCase("pg_catalog.numeric", ExpectedResult = "numeric")] + [TestCase("pg_catalog._numeric", ExpectedResult = "numeric[]")] + [TestCase("pg_catalog.decimal", ExpectedResult = "numeric")] + [TestCase("public.numeric", ExpectedResult = "numeric")] + [TestCase("public._numeric", ExpectedResult = "numeric[]")] + [TestCase("public.decimal", ExpectedResult = "decimal")] + [TestCase("public._decimal", ExpectedResult = "decimal[]")] + public string UnqualifiedDisplayName(string fullyQualifiedName) + => new DataTypeName(fullyQualifiedName).UnqualifiedDisplayName; } diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index b357417bb1..a3d62202e4 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -1137,13 +1137,39 @@ public async Task OpenConnection_when_canceled_throws_TaskCanceledException() { var builder = new NpgsqlDataSourceBuilder(ConnectionString); await using var dataSource = builder.BuildMultiHost(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); + var cancellationToken = new CancellationToken(true); var ex = Assert.ThrowsAsync(async () => { - await using var connection = await dataSource.OpenConnectionAsync(cts.Token); + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); }); - Assert.That(ex.CancellationToken, Is.EqualTo(cts.Token)); + Assert.That(ex.CancellationToken, Is.EqualTo(cancellationToken)); + } + + [Test] + public async Task OpenConnection_when_canceled_during_TryGet_throws_OperationCanceledException() + { + await using var primary1 = PgPostmasterMock.Start(state: Primary); + await using var primary2 = PgPostmasterMock.Start(state: Primary); + + var connectionString = new NpgsqlConnectionStringBuilder($"Host={primary1.Host}:{primary1.Port},{primary2.Host}:{primary2.Port}") + { + ServerCompatibilityMode = ServerCompatibilityMode.NoTypeLoading, + MaxPoolSize = 1 + }.ToString(); + await using var dataSource = new NpgsqlDataSourceBuilder(connectionString).BuildMultiHost(); + + // Exhaust the pool so that TryGetIdleOrNew returns null and we fall through to TryGet + await using var conn1 = await dataSource.OpenConnectionAsync(TargetSessionAttributes.Primary); + await using var conn2 = await dataSource.OpenConnectionAsync(TargetSessionAttributes.Primary); + + var cancellationToken = new CancellationToken(true); + + var ex = Assert.ThrowsAsync(async () => + { + await using var conn3 = await dataSource.OpenConnectionAsync(TargetSessionAttributes.Primary, cancellationToken); + }); + + Assert.That(ex.CancellationToken, Is.EqualTo(cancellationToken)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4181")] diff --git a/test/Npgsql.Tests/Support/PgPostmasterMock.cs b/test/Npgsql.Tests/Support/PgPostmasterMock.cs index d9a93531a1..426a1519c8 100644 --- a/test/Npgsql.Tests/Support/PgPostmasterMock.cs +++ b/test/Npgsql.Tests/Support/PgPostmasterMock.cs @@ -237,6 +237,8 @@ internal async ValueTask WaitForCancellationRequest() return serverOrCancellationRequest.CancellationRequest; } + internal async ValueTask SkipNextConnection() => await _pendingRequestsReader.ReadAsync(); + public async ValueTask DisposeAsync() { var endpoint = _socket.LocalEndPoint as IPEndPoint;