diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14e7ad6875..01e5549a1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -294,10 +294,19 @@ jobs: # TODO: Once test/Npgsql.Specification.Tests work, switch to just testing on the solution - name: Test run: | - dotnet test -c ${{ matrix.config }} -f ${{ matrix.test_tfm }} test/Npgsql.Tests --logger "GitHubActions;report-warnings=false" --blame-hang-timeout 30s + dotnet test -c ${{ matrix.config }} -f ${{ matrix.test_tfm }} test/Npgsql.Tests --logger "GitHubActions;report-warnings=false" --blame-crash --blame-hang-timeout 30s dotnet test -c ${{ matrix.config }} -f ${{ matrix.test_tfm }} test/Npgsql.DependencyInjection.Tests --logger "GitHubActions;report-warnings=false" shell: bash + - name: Upload Test Hang Dumps + uses: actions/upload-artifact@v6 + if: failure() + with: + name: test-hang-dumps + path: | + **/*.dmp + **/*_Sequence.xml + - name: Test Plugins if: "!startsWith(matrix.os, 'macos')" run: | diff --git a/Directory.Build.props b/Directory.Build.props index de99b4fd8c..3939a65d83 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.0 + 10.0.3 latest true enable 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.NetTopologySuite/Internal/NetTopologySuiteTypeInfoResolverFactory.cs b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeInfoResolverFactory.cs index e533d62207..2012490fb5 100644 --- a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeInfoResolverFactory.cs +++ b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeInfoResolverFactory.cs @@ -20,10 +20,11 @@ sealed class NetTopologySuiteTypeInfoResolverFactory( class Resolver : IPgTypeInfoResolver { readonly PostGisReader _gisReader; + readonly PostGisWriter _gisWriter; protected readonly bool _geographyAsDefault; TypeInfoMappingCollection? _mappings; - protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(), _gisReader, new(), _geographyAsDefault); + protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(), _gisReader, _gisWriter, _geographyAsDefault); public Resolver( CoordinateSequenceFactory? coordinateSequenceFactory, @@ -37,6 +38,10 @@ public Resolver( _geographyAsDefault = geographyAsDefault; _gisReader = new PostGisReader(coordinateSequenceFactory, precisionModel, handleOrdinates); + _gisWriter = new PostGisWriter + { + HandleOrdinates = handleOrdinates + }; } public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) diff --git a/src/Npgsql/Internal/Converters/ArrayConverter.cs b/src/Npgsql/Internal/Converters/ArrayConverter.cs index 2d6d443329..b594f19823 100644 --- a/src/Npgsql/Internal/Converters/ArrayConverter.cs +++ b/src/Npgsql/Internal/Converters/ArrayConverter.cs @@ -619,45 +619,40 @@ protected override PgConverter CreateConverter(PgConverterResolution effectiv protected override PgConverterResolution? GetEffectiveResolution(T? values, PgTypeId? expectedEffectivePgTypeId) { PgConverterResolution? resolution = null; - if (values is null) + switch (values) { - resolution = EffectiveTypeInfo.GetDefaultResolution(expectedEffectivePgTypeId); - } - else - { - switch (values) - { - case TElement[] array: - foreach (var value in array) - { - var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); - resolution ??= result; - } - break; - case List list: - foreach (var value in list) - { - var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); - resolution ??= result; - } - break; - case IList list: - foreach (var value in list) - { - var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); - resolution ??= result; - } - break; - case Array array: - foreach (var value in array) - { - var result = EffectiveTypeInfo.GetResolutionAsObject(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); - resolution ??= result; - } - break; - default: - throw new NotSupportedException(); - } + case TElement[] array: + foreach (var value in array) + { + var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); + resolution ??= result; + } + break; + case List list: + foreach (var value in list) + { + var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); + resolution ??= result; + } + break; + case IList list: + foreach (var value in list) + { + var result = EffectiveTypeInfo.GetResolution(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); + resolution ??= result; + } + break; + case Array array: + foreach (var value in array) + { + var result = EffectiveTypeInfo.GetResolutionAsObject(value, resolution?.PgTypeId ?? expectedEffectivePgTypeId); + resolution ??= result; + } + break; + case null: + break; + default: + throw new NotSupportedException(); } return resolution; diff --git a/src/Npgsql/Internal/Converters/NullableConverter.cs b/src/Npgsql/Internal/Converters/NullableConverter.cs index 57a12e005f..b4d5689da7 100644 --- a/src/Npgsql/Internal/Converters/NullableConverter.cs +++ b/src/Npgsql/Internal/Converters/NullableConverter.cs @@ -50,7 +50,7 @@ sealed class NullableConverterResolver(PgResolverTypeInfo effectiveTypeInfo) => new NullableConverter(effectiveResolution.GetConverter()); protected override PgConverterResolution? GetEffectiveResolution(T? value, PgTypeId? expectedEffectivePgTypeId) - => value is null - ? EffectiveTypeInfo.GetDefaultResolution(expectedEffectivePgTypeId) - : EffectiveTypeInfo.GetResolution(value.GetValueOrDefault(), expectedEffectivePgTypeId); + => value is { } inner + ? EffectiveTypeInfo.GetResolution(inner, expectedEffectivePgTypeId) + : null; } diff --git a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs index c14a00b608..79a82a1bfa 100644 --- a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs +++ b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs @@ -34,7 +34,7 @@ public override ValueTask ReadAsync(PgReader reader, CancellationTok { // If we don't need a read and can read buffered we delegate to our sync read method which won't do IO in such a case. if (!reader.ShouldBuffer(reader.CurrentRemaining)) - Read(reader); + return new(Read(reader)); return AsyncCore(reader, cancellationToken); @@ -166,7 +166,7 @@ public static async ValueTask ReadAsync(PgReader reader, ArraySegment var sign = reader.ReadInt16(); var scale = reader.ReadInt16(); var array = digits.Array!; - for (var i = digits.Offset; i < array.Length; i++) + for (var i = digits.Offset; i < digits.Offset + digits.Count; i++) { if (reader.ShouldBuffer(sizeof(short))) await reader.BufferAsync(sizeof(short), cancellationToken).ConfigureAwait(false); 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 617ddcd03e..663577ea1a 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -582,7 +582,8 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca { if (activity is not null) NpgsqlActivitySource.SetException(activity, e); - Break(e); + Break(e, markHostAsOfflineOnConnecting: true); + FullCleanup(); throw; } @@ -608,6 +609,10 @@ static async Task OpenCore( using var cancellationRegistration = conn.StartCancellableOperation(cancellationToken, attemptPgCancellation: false); await conn.Authenticate(username, timeout, async, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } // We handle any exception here because on Windows while receiving a response from Postgres // We might hit connection reset, in which case the actual error will be lost // And we only read some IO error @@ -668,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); @@ -676,8 +690,23 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ try { - var data = authentication.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; - if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) + byte[]? data; + NegotiateAuthenticationStatusCode statusCode; + + try + { + data = authentication.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode)!; + } + catch (TypeInitializationException) + { + // On UNIX .NET throws TypeInitializationException if it's unable to load the native library + if (isRequired) + throw new NpgsqlException("Unable to load native library to negotiate GSS encryption"); + + return GssEncryptionResult.GetCredentialFailure; + } + + if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) { // Unable to retrieve credentials // If it's required, throw an appropriate exception @@ -715,7 +744,7 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ var lengthBuffer = new byte[4]; - await WriteGssEncryptMessage(async, data, lengthBuffer).ConfigureAwait(false); + await WriteGssEncryptMessage(async, data, lengthBuffer, cancellationToken).ConfigureAwait(false); while (true) { @@ -750,7 +779,7 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ break; } - await WriteGssEncryptMessage(async, data, lengthBuffer).ConfigureAwait(false); + await WriteGssEncryptMessage(async, data, lengthBuffer, cancellationToken).ConfigureAwait(false); } _stream = new GSSStream(_stream, authentication); @@ -762,7 +791,7 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ ConnectionLogger.LogTrace("GSS encryption successful"); return GssEncryptionResult.Success; - async ValueTask WriteGssEncryptMessage(bool async, byte[] data, byte[] lengthBuffer) + async ValueTask WriteGssEncryptMessage(bool async, byte[] data, byte[] lengthBuffer, CancellationToken cancellationToken) { BinaryPrimitives.WriteInt32BigEndian(lengthBuffer, data.Length); @@ -780,7 +809,7 @@ async ValueTask WriteGssEncryptMessage(bool async, byte[] data, byte[] lengthBuf } } } - catch (Exception e) + catch (Exception e) when (e is not OperationCanceledException) { throw new NpgsqlException("Exception while performing GSS encryption", e); } @@ -1236,12 +1265,16 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, sslStream.AuthenticateAsClient(sslStreamOptions); _stream = sslStream; + sslStream = null; } - catch (Exception e) + catch (Exception e) when (e is not OperationCanceledException) { - sslStream.Dispose(); throw new NpgsqlException("Exception while performing SSL handshake", e); } + finally + { + sslStream?.Dispose(); + } ReadBuffer.Underlying = _stream; WriteBuffer.Underlying = _stream; @@ -1348,26 +1381,25 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok } else { - IPAddress[] ipAddresses; + IPAddress[] ipAddresses = []; + using var combinedCts = timeout.IsSet ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; + combinedCts?.CancelAfter(timeout.CheckAndGetTimeLeft()); + var combinedToken = combinedCts?.Token ?? cancellationToken; try { - using var combinedCts = timeout.IsSet ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; - combinedCts?.CancelAfter(timeout.CheckAndGetTimeLeft()); - var combinedToken = combinedCts?.Token ?? cancellationToken; - try - { - ipAddresses = await Dns.GetHostAddressesAsync(Host, combinedToken).ConfigureAwait(false); - } - catch (OperationCanceledException oce) when ( - oce.CancellationToken == combinedToken && !cancellationToken.IsCancellationRequested) - { - throw new TimeoutException(); - } + ipAddresses = await Dns.GetHostAddressesAsync(Host, combinedToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(timeout.HasExpired); + ThrowHelper.ThrowNpgsqlExceptionWithInnerTimeoutException("The operation has timed out"); } catch (SocketException ex) { throw new NpgsqlException(ex.Message, ex); } + endpoints = IPAddressesToEndpoints(ipAddresses, Port); } @@ -2420,14 +2452,16 @@ internal Exception UnexpectedMessageReceived(BackendMessageCode received) /// Note that fatal errors during the Open phase do *not* pass through here. /// /// The exception that caused the break. + /// Whether we treat host as down, even if we're still connecting to PostgreSQL instance. /// The exception given in for chaining calls. - internal Exception Break(Exception reason) + internal Exception Break(Exception reason, bool markHostAsOfflineOnConnecting = false) { Debug.Assert(!IsClosed); Monitor.Enter(SyncObj); - if (State == ConnectorState.Broken) + var state = State; + if (state == ConnectorState.Broken) { // We're already broken. // Exit SingleUseLock to unblock other threads (like cancellation). @@ -2462,7 +2496,9 @@ internal Exception Break(Exception reason) // Note we only set the cluster to offline and clear the pool if the connection is being broken (we're in this method), // *and* the exception indicates that the PG cluster really is down; the latter includes any IO/timeout issue, // but does not include e.g. authentication failure or timeouts with disabled cancellation. + // We also do not treat host as down if we're still connecting, as we might retry without GSS/TLS if (reason is NpgsqlException { IsTransient: true } ne && + (state != ConnectorState.Connecting || markHostAsOfflineOnConnecting) && (ne.InnerException is not TimeoutException || Settings.CancellationTimeout != -1) || reason is PostgresException pe && PostgresErrorCodes.IsCriticalFailure(pe)) { @@ -2472,7 +2508,7 @@ internal Exception Break(Exception reason) 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/Internal/TypeInfoMapping.cs b/src/Npgsql/Internal/TypeInfoMapping.cs index 64b14dff73..1fc028153f 100644 --- a/src/Npgsql/Internal/TypeInfoMapping.cs +++ b/src/Npgsql/Internal/TypeInfoMapping.cs @@ -640,7 +640,7 @@ PgTypeInfo CreateComposedPerInstance(PgTypeInfo innerTypeInfo, PgTypeInfo nullab (PgResolverTypeInfo)nullableInnerTypeInfo); return new PgResolverTypeInfo(innerTypeInfo.Options, resolver, - innerTypeInfo.Options.GetCanonicalTypeId(new DataTypeName(dataTypeName))) { SupportsWriting = false }; + innerTypeInfo.Options.GetCanonicalTypeId(new DataTypeName(dataTypeName)), unboxedType: typeof(Array)) { SupportsWriting = false }; } } diff --git a/src/Npgsql/NpgsqlBatchCommand.cs b/src/Npgsql/NpgsqlBatchCommand.cs index 17cec381b2..2812fdbead 100644 --- a/src/Npgsql/NpgsqlBatchCommand.cs +++ b/src/Npgsql/NpgsqlBatchCommand.cs @@ -172,7 +172,7 @@ internal PreparedStatement? PreparedStatement get => _preparedStatement is { State: PreparedState.Unprepared } ? _preparedStatement = null : _preparedStatement; - set => _preparedStatement = value; + private set => _preparedStatement = value; } PreparedStatement? _preparedStatement; @@ -274,7 +274,11 @@ internal void ApplyCommandComplete(CommandCompleteMessage msg) OID = msg.OID; } - internal void ResetPreparation() => ConnectorPreparedOn = null; + internal void ResetPreparation() + { + ConnectorPreparedOn = null; + PreparedStatement = null; + } internal void PopulateOutputParameters(NpgsqlDataReader reader, ILogger logger) { diff --git a/src/Npgsql/NpgsqlBinaryExporter.cs b/src/Npgsql/NpgsqlBinaryExporter.cs index 4828f0ecb1..65c616f496 100644 --- a/src/Npgsql/NpgsqlBinaryExporter.cs +++ b/src/Npgsql/NpgsqlBinaryExporter.cs @@ -7,6 +7,7 @@ using Npgsql.Internal; using Npgsql.Internal.Postgres; using NpgsqlTypes; +using InfiniteTimeout = System.Threading.Timeout; using static Npgsql.Util.Statics; namespace Npgsql; @@ -46,7 +47,7 @@ public sealed class NpgsqlBinaryExporter : ICancelable /// public TimeSpan Timeout { - set => _buf.Timeout = value; + set => _buf.Timeout = value > TimeSpan.Zero ? value : InfiniteTimeout.InfiniteTimeSpan; } Activity? _activity; diff --git a/src/Npgsql/NpgsqlBinaryImporter.cs b/src/Npgsql/NpgsqlBinaryImporter.cs index 6cd592dd06..60a1f09daf 100644 --- a/src/Npgsql/NpgsqlBinaryImporter.cs +++ b/src/Npgsql/NpgsqlBinaryImporter.cs @@ -8,6 +8,7 @@ using Npgsql.Internal; using Npgsql.Internal.Postgres; using NpgsqlTypes; +using InfiniteTimeout = System.Threading.Timeout; using static Npgsql.Util.Statics; namespace Npgsql; @@ -55,8 +56,9 @@ public TimeSpan Timeout { set { - _buf.Timeout = value; - _connector.ReadBuffer.Timeout = value; + var timeout = value > TimeSpan.Zero ? value : InfiniteTimeout.InfiniteTimeSpan; + _buf.Timeout = timeout; + _connector.ReadBuffer.Timeout = timeout; } } diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index ffbf86029f..8ddbb2e5fb 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -406,7 +406,12 @@ internal CommandState State } } - internal void ResetPreparation() => _connectorPreparedOn = null; + internal void ResetPreparation() + { + _connectorPreparedOn = null; + foreach (var s in InternalBatchCommands) + s.ResetPreparation(); + } #endregion State management @@ -873,7 +878,7 @@ async Task Unprepare(bool async, CancellationToken cancellationToken = default) if (!pStatement.IsExplicit) connector.PreparedStatementManager.AutoPrepared[pStatement.AutoPreparedSlotIndex] = null; - batchCommand.PreparedStatement = null; + batchCommand.ResetPreparation(); } } @@ -1441,8 +1446,6 @@ internal virtual async ValueTask ExecuteReader(bool async, Com { if (batchCommand.ConnectorPreparedOn != connector) { - foreach (var s in InternalBatchCommands) - s.ResetPreparation(); ResetPreparation(); goto case false; } @@ -1455,8 +1458,6 @@ internal virtual async ValueTask ExecuteReader(bool async, Com if (_connectorPreparedOn != connector) { // The command was prepared, but since then the connector has changed. Detach all prepared statements. - foreach (var s in InternalBatchCommands) - s.PreparedStatement = null; ResetPreparation(); goto case false; } 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/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 94499c14ae..55753dc7f6 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -559,8 +559,6 @@ async Task NextResult(bool async, bool isConsuming = false, CancellationTo { preparedStatement.State = PreparedState.Invalidated; Command.ResetPreparation(); - foreach (var s in Command.InternalBatchCommands) - s.ResetPreparation(); } } diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 280b32c128..dabbc978a5 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -94,15 +94,8 @@ private protected readonly Dictionary> _pendi readonly INpgsqlNameTranslator _defaultNameTranslator; readonly IDisposable? _eventSourceEvents; - internal NpgsqlDataSource( - NpgsqlConnectionStringBuilder settings, - NpgsqlDataSourceConfiguration dataSourceConfig, bool reportMetrics) + internal NpgsqlDataSource(NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig, bool reportMetrics) { - Settings = settings; - ConnectionString = settings.PersistSecurityInfo - ? settings.ToString() - : settings.ToStringWithoutPassword(); - Configuration = dataSourceConfig; (var name, @@ -128,6 +121,21 @@ internal NpgsqlDataSource( Debug.Assert(_passwordProvider is null || _passwordProviderAsync is not null); + Settings = settings; + + if (settings.PersistSecurityInfo) + { + ConnectionString = settings.ToString(); + + // The data source name is reported in tracing/metrics, so avoid leaking the password through there. + Name = name ?? settings.ToStringWithoutPassword(); + } + else + { + ConnectionString = settings.ToStringWithoutPassword(); + Name = name ?? ConnectionString; + } + _password = settings.Password; if (_periodicPasswordSuccessRefreshInterval != default) @@ -144,8 +152,6 @@ internal NpgsqlDataSource( _passwordRefreshTask = Task.Run(RefreshPassword); } - Name = name ?? ConnectionString; - // TODO this needs a rework, but for now we just avoid tracking multi-host data sources directly. if (reportMetrics) { diff --git a/src/Npgsql/NpgsqlRawCopyStream.cs b/src/Npgsql/NpgsqlRawCopyStream.cs index 45cfdf825e..bbc641ff66 100644 --- a/src/Npgsql/NpgsqlRawCopyStream.cs +++ b/src/Npgsql/NpgsqlRawCopyStream.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Npgsql.BackendMessages; using Npgsql.Internal; +using InfiniteTimeout = System.Threading.Timeout; using static Npgsql.Util.Statics; #pragma warning disable 1591 @@ -42,12 +43,12 @@ public sealed class NpgsqlRawCopyStream : Stream, ICancelable public override int WriteTimeout { get => (int) _writeBuf.Timeout.TotalMilliseconds; - set => _writeBuf.Timeout = TimeSpan.FromMilliseconds(value); + set => _writeBuf.Timeout = value > 0 ? TimeSpan.FromMilliseconds(value) : InfiniteTimeout.InfiniteTimeSpan; } public override int ReadTimeout { get => (int) _readBuf.Timeout.TotalMilliseconds; - set => _readBuf.Timeout = TimeSpan.FromMilliseconds(value); + set => _readBuf.Timeout = value > 0 ? TimeSpan.FromMilliseconds(value) : InfiniteTimeout.InfiniteTimeSpan; } /// 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/Replication/ReplicationConnection.cs b/src/Npgsql/Replication/ReplicationConnection.cs index 8583c31ce0..7bed296b5c 100644 --- a/src/Npgsql/Replication/ReplicationConnection.cs +++ b/src/Npgsql/Replication/ReplicationConnection.cs @@ -892,7 +892,7 @@ void SetTimeouts(TimeSpan readTimeout, TimeSpan writeTimeout) var connector = Connector; var readBuffer = connector.ReadBuffer; if (readBuffer != null) - readBuffer.Timeout = readTimeout > TimeSpan.Zero ? readTimeout : TimeSpan.Zero; + readBuffer.Timeout = readTimeout > TimeSpan.Zero ? readTimeout : Timeout.InfiniteTimeSpan; var writeBuffer = connector.WriteBuffer; if (writeBuffer != null) diff --git a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs index ed7afd822b..e41ede3101 100644 --- a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs +++ b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs @@ -39,7 +39,7 @@ CASE WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHE CASE WHEN ((cls.relkind = ANY (ARRAY['r'::""char"", 'p'::""char""])) OR ((cls.relkind = ANY (ARRAY['v'::""char"", 'f'::""char""])) AND pg_column_is_updatable((cls.oid)::regclass, attr.attnum, false))) - AND attr.attidentity NOT IN ('a') THEN 'true'::boolean + {(pgVersion.IsGreaterOrEqual(10) ? "AND attr.attidentity NOT IN ('a')" : "")} THEN 'true'::boolean ELSE 'false'::boolean END AS is_updatable, EXISTS ( 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.PluginTests/NetTopologySuiteTests.cs b/test/Npgsql.PluginTests/NetTopologySuiteTests.cs index 4cece1952c..cf5731c0bd 100644 --- a/test/Npgsql.PluginTests/NetTopologySuiteTests.cs +++ b/test/Npgsql.PluginTests/NetTopologySuiteTests.cs @@ -150,6 +150,85 @@ public async Task Write(Ordinates ordinates, Geometry geometry, string sqlRepres Assert.That(cmd.ExecuteScalar(), Is.True); } + [Test] + public async Task ReadWithHandleOrdinatesXY_FiltersZCoordinate() + { + // This test verifies that handleOrdinates IS respected during read operations + await using var conn = await OpenConnectionAsync(handleOrdinates: Ordinates.XY); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT ST_MakePoint(1, 2, 3)"; // Create a 3D point in SQL + + var result = (Point)cmd.ExecuteScalar()!; + + // The Z coordinate should be filtered out during reading based on handleOrdinates: XY + Assert.That(result.CoordinateSequence.HasZ, Is.False, + "Z coordinate was correctly filtered during read"); + Assert.That(result.X, Is.EqualTo(1d)); + Assert.That(result.Y, Is.EqualTo(2d)); + Assert.That(result.Z, Is.NaN, "Z coordinate should be NaN when filtered out"); + } + + [Test] + public async Task WriteWithHandleOrdinatesXY_ShouldFilterZCoordinate() + { + // This test verifies that when handleOrdinates is set to XY, + // Z coordinates are correctly filtered out during write operations. + var pointWithZ = new Point(1d, 2d, 3d); + + await using var conn = await OpenConnectionAsync(handleOrdinates: Ordinates.XY); + await using var cmd = conn.CreateCommand(); + cmd.Parameters.AddWithValue("p1", pointWithZ); + cmd.CommandText = "SELECT ST_Z(@p1::geometry)"; + + var result = cmd.ExecuteScalar(); + + // Z coordinate should be filtered out and return NULL + Assert.That(result, Is.EqualTo(DBNull.Value), + "Z coordinate should be filtered during write when handleOrdinates: Ordinates.XY"); + } + + [Test] + public async Task WriteWithHandleOrdinatesXY_ShouldFilterMCoordinate() + { + // This test verifies that when handleOrdinates is set to XY, + // M coordinates are correctly filtered out during write operations. + var pointWithM = new Point( + new DotSpatialAffineCoordinateSequence([1d, 2d], [double.NaN], [4d]), + GeometryFactory.Default); + + await using var conn = await OpenConnectionAsync(handleOrdinates: Ordinates.XY); + await using var cmd = conn.CreateCommand(); + cmd.Parameters.AddWithValue("p1", pointWithM); + cmd.CommandText = "SELECT ST_M(@p1::geometry)"; + + var result = cmd.ExecuteScalar(); + + // M coordinate should be filtered out and return NULL + Assert.That(result, Is.EqualTo(DBNull.Value), + "M coordinate should be filtered during write when handleOrdinates: Ordinates.XY"); + } + + [Test] + public async Task WriteWithHandleOrdinatesXYZ_ShouldFilterMCoordinate() + { + // This test verifies that when handleOrdinates is set to XYZ, + // M coordinates are correctly filtered out during write operations. + var pointWithZM = new Point( + new DotSpatialAffineCoordinateSequence([1d, 2d], [3d], [4d]), + GeometryFactory.Default); + + await using var conn = await OpenConnectionAsync(handleOrdinates: Ordinates.XYZ); + await using var cmd = conn.CreateCommand(); + cmd.Parameters.AddWithValue("p1", pointWithZM); + cmd.CommandText = "SELECT ST_M(@p1::geometry)"; + + var result = cmd.ExecuteScalar(); + + // M coordinate should be filtered out and return NULL + Assert.That(result, Is.EqualTo(DBNull.Value), + "M coordinate should be filtered during write when handleOrdinates: Ordinates.XYZ"); + } + [Test] public async Task Array() { diff --git a/test/Npgsql.Tests/AutoPrepareTests.cs b/test/Npgsql.Tests/AutoPrepareTests.cs index b35fe7c5d3..97a46ad277 100644 --- a/test/Npgsql.Tests/AutoPrepareTests.cs +++ b/test/Npgsql.Tests/AutoPrepareTests.cs @@ -620,6 +620,33 @@ public async Task Auto_prepared_statement_invalidation() Assert.DoesNotThrowAsync(() => command.ExecuteNonQueryAsync()); } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6432")] + public async Task Reuse_batch_with_different_connectors() + { + await using var dataSource = CreateDataSource(csb => + { + csb.MaxAutoPrepare = 10; + csb.AutoPrepareMinUsages = 2; + }); + await using var batch = new NpgsqlBatch(); + batch.BatchCommands.Add(new NpgsqlBatchCommand("SELECT 1")); + await using (var connection = await dataSource.OpenConnectionAsync()) + { + batch.Connection = connection; + + for (var i = 0; i < 2; i++) + await batch.ExecuteNonQueryAsync(); + } + + dataSource.Clear(); + + await using (var connection = await dataSource.OpenConnectionAsync()) + { + batch.Connection = connection; + await batch.ExecuteNonQueryAsync(); + } + } + void DumpPreparedStatements(NpgsqlConnection conn) { using var cmd = new NpgsqlCommand("SELECT name,statement FROM pg_prepared_statements", conn); diff --git a/test/Npgsql.Tests/BugTests.cs b/test/Npgsql.Tests/BugTests.cs index 8d46522c0f..b3cd644afd 100644 --- a/test/Npgsql.Tests/BugTests.cs +++ b/test/Npgsql.Tests/BugTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using System; using System.Data; +using System.Numerics; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -1391,4 +1392,30 @@ public async Task Bug4123() Assert.DoesNotThrowAsync(stream.FlushAsync); Assert.DoesNotThrow(stream.Flush); } + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6389")] + public async Task Composite_with_BigInteger([Values(CommandBehavior.Default, CommandBehavior.SequentialAccess)] CommandBehavior behavior) + { + await using var adminConnection = await OpenConnectionAsync(); + var type = await GetTempTypeName(adminConnection); + await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} as (value numeric)"); + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.MapComposite(type); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + await using var cmd = connection.CreateCommand(); + cmd.CommandText = $"SELECT ROW(1234567890::numeric)::{type} FROM generate_series(1, 8000)"; + await using var reader = await cmd.ExecuteReaderAsync(behavior); + while (await reader.ReadAsync()) + { + Assert.DoesNotThrowAsync(async () => await reader.GetFieldValueAsync(0)); + } + } + + class Composite_with_BigInteger_Composite + { + public BigInteger Value { get; set; } + } } diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 106daae81f..8ab2624d39 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Npgsql.Internal; using Npgsql.PostgresTypes; +using Npgsql.Tests.Support; using Npgsql.Util; using NpgsqlTypes; using NUnit.Framework; @@ -1594,6 +1595,65 @@ 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) + return; + + var csb = new NpgsqlConnectionStringBuilder(ConnectionString) + { + 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(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; + } + } + #region Physical connection initialization [Test] 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/MetricTests.cs b/test/Npgsql.Tests/MetricTests.cs index 235f8b4e27..9a8b2757e3 100644 --- a/test/Npgsql.Tests/MetricTests.cs +++ b/test/Npgsql.Tests/MetricTests.cs @@ -127,6 +127,31 @@ public async Task ConnectionMax() Assert.That(tags["db.client.connection.pool.name"], Is.EqualTo(dataSource.Name)); } + [Test] + public async Task Password_does_not_leak_via_datasource_name([Values] bool persistSecurityInfo) + { + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("Npgsql") + .AddInMemoryExporter(exportedItems) + .Build(); + + var dataSourceBuilder = base.CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.ApplicationName = "MetricsDataSource" + Interlocked.Increment(ref _dataSourceCounter); + dataSourceBuilder.ConnectionStringBuilder.PersistSecurityInfo = persistSecurityInfo; + // Do not set the data source name - this makes it default to the connection string, but without + // the password (even when Persist Security Info is true) + await using var dataSource = dataSourceBuilder.Build(); + + meterProvider.ForceFlush(); + + var metric = exportedItems.Single(m => m.Name == "db.client.connection.max"); + var point = GetFilteredPoints(metric.GetMetricPoints(), dataSource.Name).First(); + var tags = ToDictionary(point.Tags); + var connectionString = new NpgsqlConnectionStringBuilder((string)tags["db.client.connection.pool.name"]!); + Assert.That(connectionString.Password, Is.Null); + } + static Dictionary ToDictionary(ReadOnlyTagCollection tags) { var dict = new Dictionary(); diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index a98e0d60c2..b357417bb1 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -1137,10 +1137,13 @@ public async Task OpenConnection_when_canceled_throws_TaskCanceledException() { var builder = new NpgsqlDataSourceBuilder(ConnectionString); await using var dataSource = builder.BuildMultiHost(); - Assert.ThrowsAsync(async () => + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var ex = Assert.ThrowsAsync(async () => { - await using var connection = await dataSource.OpenConnectionAsync(new CancellationToken(true)); + await using var connection = await dataSource.OpenConnectionAsync(cts.Token); }); + Assert.That(ex.CancellationToken, Is.EqualTo(cts.Token)); } [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 178de2d01d..426a1519c8 100644 --- a/test/Npgsql.Tests/Support/PgPostmasterMock.cs +++ b/test/Npgsql.Tests/Support/PgPostmasterMock.cs @@ -29,6 +29,7 @@ class PgPostmasterMock : IAsyncDisposable readonly bool _completeCancellationImmediately; readonly string? _startupErrorCode; + readonly bool _breakOnGssEncryptionRequest; ChannelWriter> _pendingRequestsWriter { get; } ChannelReader> _pendingRequestsReader { get; } @@ -49,9 +50,10 @@ internal static PgPostmasterMock Start( string? connectionString = null, bool completeCancellationImmediately = true, MockState state = MockState.MultipleHostsDisabled, - string? startupErrorCode = null) + string? startupErrorCode = null, + bool breakOnGssEncryptionRequest = false) { - var mock = new PgPostmasterMock(connectionString, completeCancellationImmediately, state, startupErrorCode); + var mock = new PgPostmasterMock(connectionString, completeCancellationImmediately, state, startupErrorCode, breakOnGssEncryptionRequest); mock.AcceptClients(); return mock; } @@ -60,7 +62,8 @@ internal PgPostmasterMock( string? connectionString = null, bool completeCancellationImmediately = true, MockState state = MockState.MultipleHostsDisabled, - string? startupErrorCode = null) + string? startupErrorCode = null, + bool breakOnGssEncryptionRequest = false) { var pendingRequestsChannel = Channel.CreateUnbounded>(); _pendingRequestsReader = pendingRequestsChannel.Reader; @@ -71,6 +74,7 @@ internal PgPostmasterMock( _completeCancellationImmediately = completeCancellationImmediately; State = state; _startupErrorCode = startupErrorCode; + _breakOnGssEncryptionRequest = breakOnGssEncryptionRequest; _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); var endpoint = new IPEndPoint(IPAddress.Loopback, 0); @@ -151,6 +155,14 @@ async Task Accept(bool completeCancellationImmediat var request = readBuffer.ReadInt32(); if (request == GssRequest) { + if (_breakOnGssEncryptionRequest) + { + readBuffer.Dispose(); + writeBuffer.Dispose(); + await stream.DisposeAsync(); + return default; + } + writeBuffer.WriteByte((byte)'N'); await writeBuffer.Flush(async: true); @@ -225,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; diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 1033dc7a55..f845861366 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -811,6 +811,27 @@ public async Task Copy_ConfigureTracing() Assert.That(tags["custom_tag"], Is.EqualTo("custom_value")); } + [Test] + public async Task Password_does_not_leak_via_datasource_name([Values] bool persistSecurityInfo) + { + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.PersistSecurityInfo = persistSecurityInfo; + // Do not set the data source name - this makes it default to the connection string, but without + // the password (even when Persist Security Info is true) + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + await ExecuteScalar(connection, async, isBatch: false, query: "SELECT 42"); + + var activity = GetSingleActivity(activities, "postgresql", "postgresql"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + var connectionString = new NpgsqlConnectionStringBuilder((string)tags["db.npgsql.data_source"]!); + Assert.That(connectionString.Password, Is.Null); + } + static ActivityListener StartListener(out List activities) { var a = new List(); diff --git a/test/Npgsql.Tests/Types/ArrayTests.cs b/test/Npgsql.Tests/Types/ArrayTests.cs index 07f10330f3..d2cd4e022f 100644 --- a/test/Npgsql.Tests/Types/ArrayTests.cs +++ b/test/Npgsql.Tests/Types/ArrayTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Data; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -142,7 +143,110 @@ public async Task Value_type_array_nullabilities(ArrayNullabilityMode mode) Assert.That(value, Is.EqualTo(new int?[,]{{5, null},{6, 7}})); break; default: - throw new ArgumentOutOfRangeException(nameof(mode), mode, null); + throw new UnreachableException($"Unknown case {mode}"); + } + } + + [Test, Description("Checks that PG arrays containing nulls are returned as set via ValueTypeArrayMode.")] + [TestCase(ArrayNullabilityMode.Always)] + [TestCase(ArrayNullabilityMode.Never)] + [TestCase(ArrayNullabilityMode.PerInstance)] + public async Task Value_type_array_nullabilities_converter_resolver(ArrayNullabilityMode mode) + { + await using var dataSource = CreateDataSource(csb => + { + csb.ArrayNullabilityMode = mode; + csb.Timezone = "Europe/Berlin"; + }); + await using var conn = await dataSource.OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand( +""" +SELECT onedim, twodim FROM (VALUES +('{"1998-04-12 15:26:38+02"}'::timestamptz[],'{{"1998-04-12 15:26:38+02"},{"1998-04-13 15:26:38+02"}}'::timestamptz[][]), +('{"1998-04-14 15:26:38+02", NULL}'::timestamptz[],'{{"1998-04-14 15:26:38+02", NULL},{"1998-04-15 15:26:38+02", "1998-04-16 15:26:38+02"}}'::timestamptz[][])) AS x(onedim,twodim) +""", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + + switch (mode) + { + case ArrayNullabilityMode.Never: + reader.Read(); + var value = reader.GetValue(0); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime[]))); + Assert.That(value, Is.EqualTo(new []{new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc)})); + value = reader.GetValue(1); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime[,]))); + Assert.That(value, Is.EqualTo(new [,] + { + { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) }, + { new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Utc) } + })); + reader.Read(); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(() => reader.GetValue(0), Throws.Exception.TypeOf()); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(() => reader.GetValue(1), Throws.Exception.TypeOf()); + break; + case ArrayNullabilityMode.Always: + reader.Read(); + value = reader.GetValue(0); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[]))); + Assert.That(value, Is.EqualTo(new DateTime?[]{new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc)})); + value = reader.GetValue(1); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[,]))); + Assert.That(value, Is.EqualTo(new DateTime?[,] + { + { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) }, + { new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Utc) } + })); + reader.Read(); + value = reader.GetValue(0); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[]))); + Assert.That(value, Is.EqualTo(new DateTime?[]{ new DateTime(1998, 4, 14, 13, 26, 38, DateTimeKind.Utc), null })); + value = reader.GetValue(1); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[,]))); + Assert.That(value, Is.EqualTo(new DateTime?[,] + { + { new DateTime(1998, 4, 14, 13, 26, 38, DateTimeKind.Utc), null }, + { new DateTime(1998, 4, 15, 13, 26, 38, DateTimeKind.Utc), new DateTime(1998, 4, 16, 13, 26, 38, DateTimeKind.Utc) } + })); + break; + case ArrayNullabilityMode.PerInstance: + reader.Read(); + value = reader.GetValue(0); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime[]))); + Assert.That(value, Is.EqualTo(new []{new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc)})); + value = reader.GetValue(1); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime[,]))); + Assert.That(value, Is.EqualTo(new [,] + { + { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) }, + { new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Utc) } + })); + reader.Read(); + value = reader.GetValue(0); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[]))); + Assert.That(value, Is.EqualTo(new DateTime?[]{ new DateTime(1998, 4, 14, 13, 26, 38, DateTimeKind.Utc), null })); + value = reader.GetValue(1); + Assert.That(reader.GetFieldType(1), Is.EqualTo(typeof(Array))); + Assert.That(value.GetType(), Is.EqualTo(typeof(DateTime?[,]))); + Assert.That(value, Is.EqualTo(new DateTime?[,] + { + { new DateTime(1998, 4, 14, 13, 26, 38, DateTimeKind.Utc), null }, + { new DateTime(1998, 4, 15, 13, 26, 38, DateTimeKind.Utc), new DateTime(1998, 4, 16, 13, 26, 38, DateTimeKind.Utc) } + })); + break; + default: + throw new UnreachableException($"Unknown case {mode}"); } } diff --git a/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index fe7bb1bd27..f62a832ff3 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -472,7 +472,13 @@ public void NpgsqlParameterNpgsqlDbType_is_value_dependent_timestamp_or_timestam [Test] public async Task Array_of_nullable_timestamptz() - => await AssertType( + { + await using var datasource = CreateDataSource(csb => + { + csb.ArrayNullabilityMode = ArrayNullabilityMode.PerInstance; + csb.Timezone = "Europe/Berlin"; + }); + await AssertType(datasource, new DateTime?[] { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), @@ -480,8 +486,31 @@ public async Task Array_of_nullable_timestamptz() }, @"{""1998-04-12 15:26:38+02"",NULL}", "timestamp with time zone[]", + NpgsqlDbType.TimestampTz | NpgsqlDbType.Array); + + // Make sure delayed converter resolution works when null precedes a non-null value. + // We expect the resolution of null values to not lock in the default type timestamp. + // This would cause the subsequent non-null value to fail to convert, as it requires timestamptz. + await AssertType(datasource, + new DateTime?[] + { + null, + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) + }, + @"{NULL,""1998-04-12 15:26:38+02""}", + "timestamp with time zone[]", + NpgsqlDbType.TimestampTz | NpgsqlDbType.Array); + + await AssertType(datasource, + new DateTime?[] + { + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + }, + @"{""1998-04-12 15:26:38+02""}", + "timestamp with time zone[]", NpgsqlDbType.TimestampTz | NpgsqlDbType.Array, - isDefault: false); + isDefaultForReading: false); // we write DateTime?[], but will read DateTime[] from GetValue + } #endregion diff --git a/test/Npgsql.Tests/Types/NumericTests.cs b/test/Npgsql.Tests/Types/NumericTests.cs index 20eed3fa04..439d651559 100644 --- a/test/Npgsql.Tests/Types/NumericTests.cs +++ b/test/Npgsql.Tests/Types/NumericTests.cs @@ -212,4 +212,18 @@ public async Task NumericZero_WithScale() Assert.That(value.Scale, Is.EqualTo(2)); } + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6383")] + public async Task Read_Many_Numerics_As_BigInteger([Values(CommandBehavior.Default, CommandBehavior.SequentialAccess)] CommandBehavior behavior) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1234567890::numeric FROM generate_series(1, 8000)"; + + await using var reader = await cmd.ExecuteReaderAsync(behavior); + while (await reader.ReadAsync()) + { + Assert.DoesNotThrowAsync(async () => await reader.GetFieldValueAsync(0)); + } + } }