From 88b6bb211d56e5b5b447686e29e05d65412cd5fd Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 5 Oct 2025 21:33:18 +0200 Subject: [PATCH 01/15] Bump version to 9.0.5 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4f9ffeac69..c7ef3b5453 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 9.0.4 + 9.0.5 latest true enable From 3fb85417fffe5554ce50cc95b85521b25807d984 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 15 Oct 2025 16:18:48 +0300 Subject: [PATCH 02/15] Fix db.client.connections.create_time metric with .NET 6 (#6248) Fixes #6247 --- src/Npgsql/MetricsReporter.cs | 11 +++++++++-- src/Npgsql/PoolingDataSource.cs | 6 +----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index 83b804c2f6..bdb37db812 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -164,8 +164,15 @@ internal void ReportPendingConnectionRequestStart() internal void ReportPendingConnectionRequestStop() => PendingConnectionRequests.Add(-1, _poolNameTag); - internal void ReportConnectionCreateTime(TimeSpan duration) - => ConnectionCreateTime.Record(duration.TotalSeconds, _poolNameTag); + internal void ReportConnectionCreateTime(long startTimestamp) + { +#if NET7_0_OR_GREATER + var duration = Stopwatch.GetElapsedTime(startTimestamp); +#else + var duration = new TimeSpan((long)((Stopwatch.GetTimestamp() - startTimestamp) * StopWatchTickFrequency)); +#endif + ConnectionCreateTime.Record(duration.TotalSeconds, _poolNameTag); + } static IEnumerable> GetConnectionUsage() { diff --git a/src/Npgsql/PoolingDataSource.cs b/src/Npgsql/PoolingDataSource.cs index 46861a5c5e..dda9533374 100644 --- a/src/Npgsql/PoolingDataSource.cs +++ b/src/Npgsql/PoolingDataSource.cs @@ -268,14 +268,10 @@ bool CheckIdleConnector([NotNullWhen(true)] NpgsqlConnector? connector) try { // We've managed to increase the open counter, open a physical connections. -#if NET7_0_OR_GREATER var startTime = Stopwatch.GetTimestamp(); -#endif var connector = new NpgsqlConnector(this, conn) { ClearCounter = _clearCounter }; await connector.Open(timeout, async, cancellationToken).ConfigureAwait(false); -#if NET7_0_OR_GREATER - MetricsReporter.ReportConnectionCreateTime(Stopwatch.GetElapsedTime(startTime)); -#endif + MetricsReporter.ReportConnectionCreateTime(startTime); var i = 0; for (; i < MaxConnections; i++) From 5dfd2e4803d4b86f6927eaf85257858b2f7428e0 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 15 Oct 2025 16:30:19 +0300 Subject: [PATCH 03/15] Fix a few issues with type mappings for schema generator (#6241) Fixes #6240 (cherry picked from commit 6d2f348d9f9121815769e2ba4c1091087bae74a2) --- src/Npgsql/Schema/DbColumnSchemaGenerator.cs | 7 ++- test/Npgsql.Tests/CommandBuilderTests.cs | 56 ++++++++++++++++++++ test/Npgsql.Tests/ReaderNewSchemaTests.cs | 18 ++++++- test/Npgsql.Tests/ReaderTests.cs | 2 +- 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs index e42c6f2505..458dc725fc 100644 --- a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs +++ b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs @@ -259,8 +259,11 @@ void ColumnPostConfig(NpgsqlDbColumn column, int typeModifier) { var serializerOptions = _connection.Connector!.SerializerOptions; - column.NpgsqlDbType = column.PostgresType.DataTypeName.ToNpgsqlDbType(); - if (serializerOptions.GetDefaultTypeInfo(serializerOptions.ToCanonicalTypeId(column.PostgresType)) is { } typeInfo) + // Call GetRepresentationalType to also handle domain types + // Because NpgsqlCommandBuilder relies on NpgsqlDbType for correct type mapping + // And otherwise we'll get NpgsqlDbType.Unknown + column.NpgsqlDbType = column.PostgresType.GetRepresentationalType().DataTypeName.ToNpgsqlDbType(); + if (serializerOptions.GetTypeInfo(typeof(object), serializerOptions.ToCanonicalTypeId(column.PostgresType)) is { } typeInfo) { column.DataType = typeInfo.Type; column.IsLong = column.PostgresType.DataTypeName == DataTypeNames.Bytea; diff --git a/test/Npgsql.Tests/CommandBuilderTests.cs b/test/Npgsql.Tests/CommandBuilderTests.cs index e917b7f6b3..f134d401d2 100644 --- a/test/Npgsql.Tests/CommandBuilderTests.cs +++ b/test/Npgsql.Tests/CommandBuilderTests.cs @@ -387,4 +387,60 @@ public async Task Get_update_command_with_array_column_type() daDataAdapter.Update(dtTable); } + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6240")] + public async Task Get_update_command_with_domain_column_type() + { + await using var adminConnection = await OpenConnectionAsync(); + var domainTypeName = await GetTempTypeName(adminConnection); + + await adminConnection.ExecuteNonQueryAsync($"CREATE DOMAIN {domainTypeName} AS smallint"); + + var tableName = await CreateTempTable(adminConnection, $"id serial PRIMARY KEY, domtest {domainTypeName}"); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + + using var adapter = new NpgsqlDataAdapter($"select * from {tableName}", conn); + + var builder = new NpgsqlCommandBuilder(adapter) + { + ConflictOption = ConflictOption.CompareAllSearchableValues, + SetAllValues = true + }; + + adapter.InsertCommand = builder.GetInsertCommand(); + adapter.UpdateCommand = builder.GetUpdateCommand(); + adapter.DeleteCommand = builder.GetDeleteCommand(); + + using var dataTable = new DataTable(); + + adapter.Fill(dataTable); + + const short sval = 5; + + var newRow = dataTable.NewRow(); + newRow[1] = sval; + dataTable.Rows.Add(newRow); + + adapter.Update(dataTable); + } + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6240")] + public async Task Fill_datatable_with_array_column_type() + { + await using var connection = await OpenConnectionAsync(); + + var tableName = await CreateTempTable(connection, "id serial PRIMARY KEY, textarr text[] COLLATE pg_catalog.\"default\""); + + using var adapter = new NpgsqlDataAdapter($"select * from {tableName}", connection); + + using var dataTable = new DataTable(); + + adapter.FillSchema(dataTable, SchemaType.Source); + + adapter.MissingSchemaAction = MissingSchemaAction.Ignore; + + adapter.Fill(dataTable); + } } diff --git a/test/Npgsql.Tests/ReaderNewSchemaTests.cs b/test/Npgsql.Tests/ReaderNewSchemaTests.cs index 79b4b38ddb..1f245e3c5c 100644 --- a/test/Npgsql.Tests/ReaderNewSchemaTests.cs +++ b/test/Npgsql.Tests/ReaderNewSchemaTests.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Data; using System.Data.Common; using System.Linq; @@ -450,6 +451,19 @@ public async Task DataType_with_composite() Assert.That(columns[1].UdtAssemblyQualifiedName, Is.EqualTo(typeof(SomeComposite).AssemblyQualifiedName)); } + [Test] + public async Task DataType_with_array() + { + using var conn = await OpenConnectionAsync(); + var table = await CreateTempTable(conn, "foo INTEGER[]"); + + using var cmd = new NpgsqlCommand($"SELECT foo, ARRAY[1::INTEGER, 2::INTEGER] FROM {table}", conn); + using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); + var columns = await GetColumnSchema(reader); + Assert.That(columns[0].DataType, Is.SameAs(typeof(Array))); + Assert.That(columns[1].DataType, Is.SameAs(typeof(Array))); + } + [Test] public async Task UdtAssemblyQualifiedName() { @@ -673,6 +687,8 @@ public async Task Domain_type() var pgType = domainSchema.PostgresType; Assert.That(pgType, Is.InstanceOf()); Assert.That(((PostgresDomainType)pgType).BaseType.Name, Is.EqualTo("character varying")); + // For domains we should return the underlying type + Assert.That(domainSchema.NpgsqlDbType, Is.EqualTo(NpgsqlTypes.NpgsqlDbType.Varchar)); } [Test] diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index 9c912f1164..2845147ece 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -304,7 +304,7 @@ public async Task GetFieldType_SchemaOnly() { await using var conn = await OpenConnectionAsync(); await using var cmd = new NpgsqlCommand(@"SELECT 1::INT4 AS some_column", conn); - await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); + await using var reader = await cmd.ExecuteReaderAsync(Behavior | CommandBehavior.SchemaOnly); reader.Read(); Assert.That(reader.GetFieldType(0), Is.SameAs(typeof(int))); } From 0d526a369a6bd2ee4f01ad85f65e0a2814f96b1a Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 27 Oct 2025 21:59:28 +0100 Subject: [PATCH 04/15] Respect configured schemas in enum field loading (#6268) Closes #6246 (cherry picked from commit 62fd0ad621dab74efdfde07a0b698b7d5a9f86d0) --- src/Npgsql/PostgresDatabaseInfo.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Npgsql/PostgresDatabaseInfo.cs b/src/Npgsql/PostgresDatabaseInfo.cs index 6cd4f2a5fe..1c1b518a3f 100644 --- a/src/Npgsql/PostgresDatabaseInfo.cs +++ b/src/Npgsql/PostgresDatabaseInfo.cs @@ -149,8 +149,8 @@ LEFT JOIN pg_class AS elemcls ON (elemcls.oid = elemtyp.typrelid) ) AS t JOIN pg_namespace AS ns ON (ns.oid = typnamespace) WHERE - {(schemaListSqlFragment is not null ? $"(ns.nspname IN ({BuiltinSchemaListSqlFragment}{(schemaListSqlFragment.Length > 0 ? $", {schemaListSqlFragment}" : "")}){(hasTypeCategory ? " OR typcategory = 'U'" : "" )}) AND (" : "(")} - typtype IN ('b', 'r', 'm', 'e', 'd') OR -- Base, range, multirange, enum, domain + {(schemaListSqlFragment is not null ? $"(ns.nspname IN ({schemaListSqlFragment}){(hasTypeCategory ? " OR typcategory = 'U'" : "" )}) AND " : "")} + (typtype IN ('b', 'r', 'm', 'e', 'd') OR -- Base, range, multirange, enum, domain (typtype = 'c' AND {(loadTableComposites ? $"ns.nspname NOT IN ({BuiltinSchemaListSqlFragment})" : "relkind='c'")}) OR -- User-defined free-standing composites (not table composites) by default (typtype = 'p' AND typname IN ('record', 'void', 'unknown')) OR -- Some special supported pseudo-types (typtype = 'a' AND ( -- Array of... @@ -178,17 +178,19 @@ JOIN pg_class AS cls ON (cls.oid = typ.typrelid) JOIN pg_attribute AS att ON (att.attrelid = typ.typrelid) WHERE (typ.typtype = 'c' AND {(loadTableComposites ? $"ns.nspname NOT IN ({BuiltinSchemaListSqlFragment})" : "cls.relkind='c'")}) AND - {(schemaListSqlFragment is not null ? $"(ns.nspname IN ({BuiltinSchemaListSqlFragment}{(schemaListSqlFragment.Length > 0 ? $", {schemaListSqlFragment}" : "")})) AND " : "")} + {(schemaListSqlFragment is not null ? $"(ns.nspname IN ({schemaListSqlFragment})) AND " : "")} attnum > 0 AND -- Don't load system attributes NOT attisdropped ORDER BY typ.oid, att.attnum;"; - static string GenerateLoadEnumFieldsQuery(bool withEnumSortOrder) + static string GenerateLoadEnumFieldsQuery(bool withEnumSortOrder, string? schemaListSqlFragment) => $@" -- Load enum fields -SELECT pg_type.oid, enumlabel +SELECT typ.oid, enumlabel FROM pg_enum -JOIN pg_type ON pg_type.oid=enumtypid +JOIN pg_type AS typ ON typ.oid = enumtypid +JOIN pg_namespace AS ns ON ns.oid = typ.typnamespace +{(schemaListSqlFragment is not null ? $"WHERE (ns.nspname IN ({schemaListSqlFragment}))" : "")} ORDER BY oid{(withEnumSortOrder ? ", enumsortorder" : "")};"; /// @@ -213,11 +215,10 @@ internal async Task> LoadBackendTypes(NpgsqlConnector conn, N string? schemaListSqlFragment = null; if (typeLoading.TypeLoadingSchemas is not null) { - var builder = new StringBuilder(); + var builder = new StringBuilder(BuiltinSchemaListSqlFragment); for (var i = 0; i < typeLoading.TypeLoadingSchemas.Length; i++) { - if (i > 0) - builder.Append(", "); + builder.Append(", "); var schema = typeLoading.TypeLoadingSchemas[i]; builder.Append('\''); builder.Append(EscapeLiteral(schema)); @@ -230,7 +231,7 @@ internal async Task> LoadBackendTypes(NpgsqlConnector conn, N var loadTypesQuery = GenerateLoadTypesQuery(SupportsRangeTypes, SupportsMultirangeTypes, loadTableComposites, schemaListSqlFragment, HasTypeCategory); var loadCompositeTypesQuery = GenerateLoadCompositeTypesQuery(loadTableComposites, schemaListSqlFragment); var loadEnumFieldsQuery = SupportsEnumTypes - ? GenerateLoadEnumFieldsQuery(HasEnumSortOrder) + ? GenerateLoadEnumFieldsQuery(HasEnumSortOrder, schemaListSqlFragment) : string.Empty; timeout.CheckAndApply(conn); From cd9a7ab9fde6e2d0e10b061739254151ae3da44c Mon Sep 17 00:00:00 2001 From: Artem Serostanov Date: Wed, 29 Oct 2025 17:53:42 +0300 Subject: [PATCH 05/15] Incorrect multi-threading synchronization in NpgsqlDataSource.UpdateDatabaseState() (#6114) Co-authored-by: Serostanov Artem Sergeevich (cherry picked from commit 153b91fc656ffef2f62bece778f6d9a88dbbbac3) --- src/Npgsql/NpgsqlDataSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 225722f3eb..5dec2997ac 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -426,7 +426,7 @@ internal DatabaseState UpdateDatabaseState( var databaseStateInfo = _databaseStateInfo; if (!ignoreTimeStamp && timeStamp <= databaseStateInfo.TimeStamp) - return _databaseStateInfo.State; + return databaseStateInfo.State; _databaseStateInfo = new(newState, new NpgsqlTimeout(stateExpiration), timeStamp); From 22edc2368a4ca2da769a3c3dd233b5d08dd01392 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Wed, 29 Oct 2025 17:23:19 +0100 Subject: [PATCH 06/15] Fix inclusive infinity upper bound interval conversion (#6270) Closes #6080 (cherry picked from commit 2ef750c1a4ed5dd5d7e19f9cb7c840ebb23a5fb8) --- .../Internal/IntervalConverter.cs | 9 +++++++-- ...aTimeTypeInfoResolverFactory.Multirange.cs | 4 ++-- .../NodaTimeTypeInfoResolverFactory.Range.cs | 2 +- .../NodaTimeInfinityTests.cs | 20 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Npgsql.NodaTime/Internal/IntervalConverter.cs b/src/Npgsql.NodaTime/Internal/IntervalConverter.cs index 7da4aa401c..f062079a4a 100644 --- a/src/Npgsql.NodaTime/Internal/IntervalConverter.cs +++ b/src/Npgsql.NodaTime/Internal/IntervalConverter.cs @@ -6,7 +6,7 @@ namespace Npgsql.NodaTime.Internal; -public class IntervalConverter(PgConverter> rangeConverter) : PgStreamingConverter +sealed class IntervalConverter(PgConverter> rangeConverter, bool dateTimeInfinityConversions) : PgStreamingConverter { public override Interval Read(PgReader reader) => Read(async: false, reader, CancellationToken.None).GetAwaiter().GetResult(); @@ -27,7 +27,12 @@ async ValueTask Read(bool async, PgReader reader, CancellationToken ca : range.LowerBoundIsInclusive ? range.LowerBound : range.LowerBound + Duration.Epsilon; - Instant? end = range.UpperBoundInfinite + // For ranges with element types with infinity values (datetime, date etc.) an + // inclusive lower/upper bound causes their -/+ infinity (respectively) to fall within the range. + // If those values are returned for such a range postgres will not mark the affected bound as infinite accordingly. + // This is documented in https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-INFINITE + // As NodaTime uses an exclusive upper bound we must consider this case as being another form of infinity (null). + Instant? end = range.UpperBoundInfinite || (dateTimeInfinityConversions && range.UpperBoundIsInclusive && range.UpperBound == Instant.MaxValue) ? null : range.UpperBoundIsInclusive ? range.UpperBound + Duration.Epsilon diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Multirange.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Multirange.cs index 42c6360dad..fdd8d4c78f 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Multirange.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Multirange.cs @@ -31,12 +31,12 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mappings.AddType(TimestampTzMultirangeDataTypeName, static (options, mapping, _) => mapping.CreateInfo(options, CreateArrayMultirangeConverter(new IntervalConverter( - CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options)), options)), + CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options), options.EnableDateTimeInfinityConversions), options)), isDefault: true); mappings.AddType>(TimestampTzMultirangeDataTypeName, static (options, mapping, _) => mapping.CreateInfo(options, CreateListMultirangeConverter(new IntervalConverter( - CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options)), options))); + CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options), options.EnableDateTimeInfinityConversions), options))); mappings.AddType[]>(TimestampTzMultirangeDataTypeName, static (options, mapping, _) => mapping.CreateInfo(options, diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Range.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Range.cs index f62669333c..8958f88846 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Range.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.Range.cs @@ -31,7 +31,7 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, new IntervalConverter( - CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options))), + CreateRangeConverter(new InstantConverter(options.EnableDateTimeInfinityConversions), options), options.EnableDateTimeInfinityConversions)), isDefault: true); mappings.AddStructType>(TimestampTzRangeDataTypeName, static (options, mapping, _) => mapping.CreateInfo(options, diff --git a/test/Npgsql.PluginTests/NodaTimeInfinityTests.cs b/test/Npgsql.PluginTests/NodaTimeInfinityTests.cs index 75559169f0..e8f8036ada 100644 --- a/test/Npgsql.PluginTests/NodaTimeInfinityTests.cs +++ b/test/Npgsql.PluginTests/NodaTimeInfinityTests.cs @@ -361,6 +361,26 @@ public async Task Interval_convert_infinity() } } + [Test] + public async Task Inclusive_End_Range_Infinity_read() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand( + "SELECT tstzrange('-infinity', 'infinity','[]') as val", conn); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + if (Statics.DisableDateTimeInfinityConversions) + { + Assert.That(() => reader[0], Throws.Exception.TypeOf()); + } + else + { + Assert.That(reader[0], Is.EqualTo(new Interval(Instant.MinValue, null))); + } + } + protected override NpgsqlDataSource DataSource { get; } public NodaTimeInfinityTests(bool disableDateTimeInfinityConversions) From 04304d6de51d76e8f2fd50dfb2ef521774ba1222 Mon Sep 17 00:00:00 2001 From: Trivalik <3148279+trivalik@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:08:54 +0100 Subject: [PATCH 07/15] handles SSL ConnectionReset on Windows; fixes #6274 (#6287) Fixes #6274 (cherry picked from commit ed512e5297894f9cd9751b540ac6de9f1f493388) --- src/Npgsql/Internal/NpgsqlConnector.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 5f8add7836..d830e43211 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -590,9 +590,7 @@ static async Task OpenCore( { await conn.Authenticate(username, timeout, async, cancellationToken).ConfigureAwait(false); } - catch (PostgresException e) - when (e.SqlState == PostgresErrorCodes.InvalidAuthorizationSpecification && - (sslMode == SslMode.Prefer && conn.IsSecure || sslMode == SslMode.Allow && !conn.IsSecure)) + catch when (sslMode == SslMode.Prefer && conn.IsSecure || sslMode == SslMode.Allow && !conn.IsSecure) { cancellationRegistration.Dispose(); Debug.Assert(!conn.IsBroken); From 8bb2db23dea224b80ecb4d7e7c651e1f2cbff236 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 6 Nov 2025 13:39:57 +0300 Subject: [PATCH 08/15] Fix returning properties from NpgsqlConnectionStringBuilder.GetProperties (#6290) Fixes #6289 (cherry picked from commit ce11aaa601ca7ba4d1974ee59d4ef791f3cb71e0) --- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index f662dd4a83..8ba812279e 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -1584,9 +1584,22 @@ protected override void GetProperties(Hashtable propertyDescriptors) foreach (var value in propertyDescriptors.Values) { var d = (PropertyDescriptor)value; + var isConnectionStringProperty = false; + var isObsolete = false; foreach (var attribute in d.Attributes) - if (attribute is NpgsqlConnectionStringPropertyAttribute or ObsoleteAttribute) - toRemove.Add(d); + { + if (attribute is NpgsqlConnectionStringPropertyAttribute) + { + isConnectionStringProperty = true; + } + else if (attribute is ObsoleteAttribute) + { + isObsolete = true; + } + } + + if (!isConnectionStringProperty || isObsolete) + toRemove.Add(d); } foreach (var o in toRemove) From 603024f40e75c1f861251617d776f47a3197ec46 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 23 Dec 2025 12:20:44 +0300 Subject: [PATCH 09/15] Fix reading numerics as BigInteger (#6385) Fixes #6383 (cherry picked from commit 381b0fada639e6fbc8fcd2df5f9c320c09745afc) --- .../Converters/Primitive/NumericConverters.cs | 2 +- test/Npgsql.Tests/Types/NumericTests.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs index 16a3c67639..90786715b7 100644 --- a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs +++ b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs @@ -221,7 +221,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/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)); + } + } } From bf3dd9bf65676fbf72249ad66a1aa78ecfcf698d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 29 Dec 2025 14:55:37 +0100 Subject: [PATCH 10/15] Do not include password in data source name with Persist Security Info=true (#6395) Closes #6394 (cherry picked from commit 847d69f641a70e5abdb9c67fbcbb51f6d34fe832) --- src/Npgsql/NpgsqlDataSource.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 5dec2997ac..f5ca93d107 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -86,11 +86,6 @@ internal NpgsqlDataSource( NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig) { - Settings = settings; - ConnectionString = settings.PersistSecurityInfo - ? settings.ToString() - : settings.ToStringWithoutPassword(); - Configuration = dataSourceConfig; (var name, @@ -118,6 +113,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; + } + _resolverChain = resolverChain; _password = settings.Password; @@ -134,7 +144,6 @@ internal NpgsqlDataSource( _passwordRefreshTask = Task.Run(RefreshPassword); } - Name = name ?? ConnectionString; MetricsReporter = new MetricsReporter(this); } From 10c40c7798cb653ee1f56a866879eb22e15c578a Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 28 Dec 2025 13:56:33 +0300 Subject: [PATCH 11/15] Fix reading BigInteger with composites (#6390) Fixes #6389 (cherry picked from commit 3c535681c828565fbd7bb1a9c7c1b3ae68b26b7a) --- .../Converters/Primitive/NumericConverters.cs | 2 +- test/Npgsql.Tests/BugTests.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs index 90786715b7..0addea42d2 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); diff --git a/test/Npgsql.Tests/BugTests.cs b/test/Npgsql.Tests/BugTests.cs index 2e3dfa97fc..16d72ce5e6 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; } + } } From d5c5bff0a6a02dc4440343f7b65cd630ae720780 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Tue, 10 Feb 2026 19:32:29 +0100 Subject: [PATCH 12/15] Fix per instance nullability converter resolver info code (#6435) (cherry picked from commit aed30af7895487e42136c9a8735bf4ad21de40fe) --- src/Npgsql/Internal/TypeInfoMapping.cs | 2 +- test/Npgsql.Tests/Types/ArrayTests.cs | 106 ++++++++++++++++++++++- test/Npgsql.Tests/Types/DateTimeTests.cs | 20 ++++- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/Npgsql/Internal/TypeInfoMapping.cs b/src/Npgsql/Internal/TypeInfoMapping.cs index c8439de6ac..16d564855c 100644 --- a/src/Npgsql/Internal/TypeInfoMapping.cs +++ b/src/Npgsql/Internal/TypeInfoMapping.cs @@ -636,7 +636,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/test/Npgsql.Tests/Types/ArrayTests.cs b/test/Npgsql.Tests/Types/ArrayTests.cs index 3202bd0ba9..6ed6c8c258 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 815514031a..e2cda1c7f8 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -475,7 +475,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), @@ -483,8 +489,18 @@ public async Task Array_of_nullable_timestamptz() }, @"{""1998-04-12 15:26:38+02"",NULL}", "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 From cc8772d0354b30a073620da350c8e0a3df18a33a Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Thu, 12 Mar 2026 06:41:50 +0100 Subject: [PATCH 13/15] Fix delayed converter resolution for nullables (#6453) (cherry picked from commit 013e7717ab8e37228d3f06dd312221e25057572b) --- .../Internal/Converters/ArrayConverter.cs | 71 +++++++++---------- .../Internal/Converters/NullableConverter.cs | 6 +- test/Npgsql.Tests/Types/DateTimeTests.cs | 12 ++++ 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/Npgsql/Internal/Converters/ArrayConverter.cs b/src/Npgsql/Internal/Converters/ArrayConverter.cs index 262f748651..39a6f253a1 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/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index e2cda1c7f8..708eda3763 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -491,6 +491,18 @@ await AssertType(datasource, "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[]"); + await AssertType(datasource, new DateTime?[] { From 6c91ba89c80e4f8785e70c286decf2ca2b1ace26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Harrtell?= Date: Thu, 12 Mar 2026 08:38:39 +0100 Subject: [PATCH 14/15] Respect handleOrdinates also when writing geometry (#6380) (cherry picked from commit b18dd1773909c929128b825ce5ff4a21d1a85cec) --- ...NetTopologySuiteTypeInfoResolverFactory.cs | 7 +- .../NetTopologySuiteTests.cs | 79 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) 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/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() { From 45e754067159c79bfb491efee8af4fcdbf8ecc7e Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 12 Mar 2026 17:40:18 +0200 Subject: [PATCH 15/15] Fix test --- test/Npgsql.Tests/Types/DateTimeTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index 708eda3763..8fc6c8d1d2 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -501,7 +501,8 @@ await AssertType(datasource, new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) }, @"{NULL,""1998-04-12 15:26:38+02""}", - "timestamp with time zone[]"); + "timestamp with time zone[]", + NpgsqlDbType.TimestampTz | NpgsqlDbType.Array); await AssertType(datasource, new DateTime?[]