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 9a09fe1218..9ba57dbc8e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.1 + 10.0.2 latest true enable 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/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 41a42950a7..bc4b2a1416 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -582,7 +582,7 @@ 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); throw; } @@ -608,6 +608,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 @@ -676,7 +680,22 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ try { - var data = authentication.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; + 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 != NegotiateAuthenticationStatusCode.ContinueNeeded) { // Unable to retrieve credentials @@ -715,7 +734,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 +769,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 +781,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 +799,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 +1255,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 +1371,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) - { - cancellationToken.ThrowIfCancellationRequested(); - 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 +2442,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 +2486,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)) { 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/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/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/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/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..f776e3bc4b 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,34 @@ public async Task Sync_open_blocked_same_thread() } } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6427")] + public async Task Gss_encryption_retry_does_not_clear_pool() + { + if (IsMultiplexing) + return; + + var csb = new NpgsqlConnectionStringBuilder(ConnectionString) + { + GssEncryptionMode = GssEncryptionMode.Prefer + }; + // 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); + + int processID; + await using (var conn = await dataSource.OpenConnectionAsync()) + { + processID = conn.ProcessID; + } + + // 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)); + } + } + #region Physical connection initialization [Test] 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/Support/PgPostmasterMock.cs b/test/Npgsql.Tests/Support/PgPostmasterMock.cs index 178de2d01d..d9a93531a1 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); 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)); + } + } }