From 94de20fed2e7e64a1eb6f26c9fc044131a362958 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 18 Nov 2024 16:02:20 +0100 Subject: [PATCH 001/155] Bump version to 10.0.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0117142918..50cb8595eb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 9.0.0 + 10.0.0 latest true enable From 1d93236d0260c26cd1b9dec84ad3605c7efcb821 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Tue, 19 Nov 2024 17:57:26 +0700 Subject: [PATCH 002/155] STJ 9.0 alternative approach (#5941) --- Directory.Packages.props | 4 +-- .../JsonDynamicTypeInfoResolverFactory.cs | 12 +++++++-- src/Npgsql/Npgsql.csproj | 1 - test/Directory.Build.props | 2 +- test/Npgsql.Tests/Types/JsonDynamicTests.cs | 25 +++++++++++++++++++ 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 99cc5afec3..6f250d7c83 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - + @@ -44,4 +44,4 @@ - \ No newline at end of file + diff --git a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs index bc882382c6..6384474cd7 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs @@ -53,6 +53,14 @@ JsonSerializerOptions SerializerOptions readonly Type[] _jsonClrTypes = jsonClrTypes ?? []; TypeInfoMappingCollection? _mappings; +#if NET9_0_OR_GREATER + static Func AllowOutOfOrderMetadataProperties { get; } = options => options.AllowOutOfOrderMetadataProperties; +#else + static Func AllowOutOfOrderMetadataProperties { get; } = + typeof(JsonSerializerOptions).GetProperty("AllowOutOfOrderMetadataProperties") is { } prop && prop.GetGetMethod() is { } getProp + ? getProp.CreateDelegate>() + : _ => false; +#endif protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(), _jsonbClrTypes, _jsonClrTypes, SerializerOptions); public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) @@ -98,7 +106,7 @@ void AddUserMappings(bool jsonb, Type[] clrTypes) // For jsonb we can't properly support polymorphic serialization unless the SerializerOptions.AllowOutOfOrderMetadataProperties is `true`. // If `jsonb` AND `AllowOutOfOrderMetadataProperties` is `false`, use `derived.DerivedType` as the base type for the converter, // this causes STJ to stop serializing the "$type" field; essentially disabling the feature. - var baseType = jsonb && !serializerOptions.AllowOutOfOrderMetadataProperties ? derived.DerivedType : jsonType; + var baseType = jsonb && !AllowOutOfOrderMetadataProperties(serializerOptions) ? derived.DerivedType : jsonType; dynamicMappings.AddMapping(derived.DerivedType, dataTypeName, factory: (options, mapping, _) => mapping.CreateInfo(options, CreateSystemTextJsonConverter(mapping.Type, jsonb, options.TextEncoding, serializerOptions, baseType))); @@ -125,7 +133,7 @@ void AddUserMappings(bool jsonb, Type[] clrTypes) // For jsonb we can't properly support polymorphic serialization unless the SerializerOptions.AllowOutOfOrderMetadataProperties is `true`. // If `jsonb` AND `AllowOutOfOrderMetadataProperties` is `false`, use `mapping.Type` as the base type for the converter, // this causes STJ to stop serializing the "$type" field; essentially disabling the feature. - var baseType = jsonb && !SerializerOptions.AllowOutOfOrderMetadataProperties ? mapping.Type : typeof(object); + var baseType = jsonb && !AllowOutOfOrderMetadataProperties(SerializerOptions) ? mapping.Type : typeof(object); return mapping.CreateInfo(options, CreateSystemTextJsonConverter(mapping.Type, jsonb, options.TextEncoding, SerializerOptions, baseType)); diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 4c5042b340..426eb06bcd 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -22,7 +22,6 @@ - diff --git a/test/Directory.Build.props b/test/Directory.Build.props index b51b1c04ba..1e2132817e 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - net8.0 + net8.0;net9.0 false diff --git a/test/Npgsql.Tests/Types/JsonDynamicTests.cs b/test/Npgsql.Tests/Types/JsonDynamicTests.cs index b6f3004a64..21ff2700da 100644 --- a/test/Npgsql.Tests/Types/JsonDynamicTests.cs +++ b/test/Npgsql.Tests/Types/JsonDynamicTests.cs @@ -220,11 +220,17 @@ await AssertType( [Test] public async Task Poco_polymorphic_mapping() { +#if !NET9_0_OR_GREATER + if (IsJsonb) + return; +#endif await using var dataSource = CreateDataSource(builder => { var types = new[] {typeof(WeatherForecast)}; builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = true }) +#endif .EnableDynamicJson(jsonClrTypes: IsJsonb ? [] : types, jsonbClrTypes: !IsJsonb ? [] : types); }); @@ -249,11 +255,17 @@ public async Task Poco_polymorphic_mapping() [Test] public async Task Poco_polymorphic_mapping_read_parents() { +#if !NET9_0_OR_GREATER + if (IsJsonb) + return; +#endif await using var dataSource = CreateDataSource(builder => { var types = new[] {typeof(WeatherForecast)}; builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = true }) +#endif .EnableDynamicJson(jsonClrTypes: IsJsonb ? [] : types, jsonbClrTypes: !IsJsonb ? [] : types); }); @@ -293,7 +305,9 @@ public async Task Poco_exact_polymorphic_mapping() { var types = new[] {typeof(ExtendedDerivedWeatherForecast)}; builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = true }) +#endif .EnableDynamicJson(jsonClrTypes: IsJsonb ? [] : types, jsonbClrTypes: !IsJsonb ? [] : types); }); @@ -318,10 +332,17 @@ public async Task Poco_exact_polymorphic_mapping() [Test] public async Task Poco_unspecified_polymorphic_mapping() { +#if !NET9_0_OR_GREATER + if (IsJsonb) + return; +#endif + await using var dataSource = CreateDataSource(builder => { builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = true }) +#endif .EnableDynamicJson(); }); @@ -360,7 +381,9 @@ public async Task Poco_polymorphic_mapping_without_AllowOutOfOrderMetadataProper { var types = new[] {typeof(WeatherForecast)}; builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = false }) +#endif .EnableDynamicJson(jsonClrTypes: IsJsonb ? [] : types, jsonbClrTypes: !IsJsonb ? [] : types); }); @@ -413,7 +436,9 @@ public async Task Poco_unspecified_polymorphic_mapping_without_AllowOutOfOrderMe await using var dataSource = CreateDataSource(builder => { builder +#if NET9_0_OR_GREATER .ConfigureJsonOptions(new() { AllowOutOfOrderMetadataProperties = false }) +#endif .EnableDynamicJson(); }); From b0dde48c52fb039b9ff8566d13e6c712fb391bcb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 20 Nov 2024 12:42:24 +0200 Subject: [PATCH 003/155] Remove support for net6.0 (#5947) Closes #5946 --- global.json | 2 +- src/Directory.Build.props | 6 - .../Npgsql.DependencyInjection.csproj | 5 +- src/Npgsql.GeoJSON/Npgsql.GeoJSON.csproj | 3 +- src/Npgsql.Json.NET/Npgsql.Json.NET.csproj | 3 +- .../Npgsql.NetTopologySuite.csproj | 3 +- src/Npgsql.NodaTime/Npgsql.NodaTime.csproj | 3 +- .../Npgsql.OpenTelemetry.csproj | 3 +- .../Internal/InternalCharConverter.cs | 28 +--- .../Internal/Converters/MoneyConverter.cs | 63 +------- .../Networking/IPNetworkConverter.cs | 6 +- .../Converters/Primitive/DoubleConverter.cs | 28 +--- .../Converters/Primitive/GuidUuidConverter.cs | 24 +-- .../Converters/Primitive/Int2Converter.cs | 57 +------ .../Converters/Primitive/Int4Converter.cs | 56 +------ .../Converters/Primitive/Int8Converter.cs | 57 +------ .../Converters/Primitive/NumericConverters.cs | 61 +------ .../Internal/Converters/Primitive/PgMoney.cs | 4 - .../Converters/Primitive/PgNumeric.cs | 5 - .../Converters/Primitive/RealConverter.cs | 28 +--- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 17 +- .../Internal/NpgsqlConnector.OldAuth.cs | 153 ------------------ src/Npgsql/Internal/NpgsqlConnector.cs | 12 -- .../JsonDynamicTypeInfoResolverFactory.cs | 7 +- .../NetworkTypeInfoResolverFactory.cs | 2 - src/Npgsql/KerberosUsernameProvider.cs | 4 - src/Npgsql/MetricsReporter.cs | 7 +- src/Npgsql/Npgsql.csproj | 9 +- src/Npgsql/NpgsqlBatchCommand.cs | 21 +-- src/Npgsql/NpgsqlDataSource.cs | 7 +- src/Npgsql/NpgsqlDataSourceBuilder.cs | 2 - src/Npgsql/NpgsqlDataSourceConfiguration.cs | 7 +- src/Npgsql/NpgsqlException.cs | 2 - src/Npgsql/NpgsqlFactory.cs | 2 - src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 9 +- src/Npgsql/PoolingDataSource.cs | 4 - src/Npgsql/PostgresException.cs | 2 - src/Npgsql/Shims/DbDataSource.cs | 70 -------- src/Npgsql/Shims/ExperimentalAttribute.cs | 21 --- src/Npgsql/Shims/MemoryExtensions.cs | 20 --- src/Npgsql/Shims/StreamExtensions.cs | 38 ----- src/Npgsql/Shims/UnreachableException.cs | 39 ----- src/Shared/CodeAnalysis.cs | 83 ---------- .../DistributedTransactionTests.cs | 4 - 44 files changed, 36 insertions(+), 951 deletions(-) delete mode 100644 src/Npgsql/Internal/NpgsqlConnector.OldAuth.cs delete mode 100644 src/Npgsql/Shims/DbDataSource.cs delete mode 100644 src/Npgsql/Shims/ExperimentalAttribute.cs delete mode 100644 src/Npgsql/Shims/MemoryExtensions.cs delete mode 100644 src/Npgsql/Shims/StreamExtensions.cs delete mode 100644 src/Npgsql/Shims/UnreachableException.cs delete mode 100644 src/Shared/CodeAnalysis.cs diff --git a/global.json b/global.json index 67db748a1b..733b653c18 100644 --- a/global.json +++ b/global.json @@ -2,6 +2,6 @@ "sdk": { "version": "9.0.100", "rollForward": "latestMajor", - "allowPrerelease": "false" + "allowPrerelease": false } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9d07823223..b94a8a91bd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,14 +3,8 @@ true - - true - - - - diff --git a/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj b/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj index 357003cf07..aa33763975 100644 --- a/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj +++ b/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj @@ -2,10 +2,7 @@ Shay Rojansky - - - net6.0;net8.0 - net8.0 + net8.0 npgsql;postgresql;postgres;ado;ado.net;database;sql;di;dependency injection README.md diff --git a/src/Npgsql.GeoJSON/Npgsql.GeoJSON.csproj b/src/Npgsql.GeoJSON/Npgsql.GeoJSON.csproj index 072feabea3..7b76bde10f 100644 --- a/src/Npgsql.GeoJSON/Npgsql.GeoJSON.csproj +++ b/src/Npgsql.GeoJSON/Npgsql.GeoJSON.csproj @@ -3,8 +3,7 @@ Yoh Deadfall;Shay Rojansky GeoJSON plugin for Npgsql, allowing mapping of PostGIS geometry types to GeoJSON types. npgsql;postgresql;postgres;postgis;geojson;spatial;ado;ado.net;database;sql - net6.0 - net8.0 + net8.0 $(NoWarn);NPG9001 diff --git a/src/Npgsql.Json.NET/Npgsql.Json.NET.csproj b/src/Npgsql.Json.NET/Npgsql.Json.NET.csproj index 67109a48da..e126980ad1 100644 --- a/src/Npgsql.Json.NET/Npgsql.Json.NET.csproj +++ b/src/Npgsql.Json.NET/Npgsql.Json.NET.csproj @@ -3,8 +3,7 @@ Shay Rojansky Json.NET plugin for Npgsql, allowing transparent serialization/deserialization of JSON objects directly to and from the database. npgsql;postgresql;json;postgres;ado;ado.net;database;sql - net6.0 - net8.0 + net8.0 enable $(NoWarn);NPG9001 diff --git a/src/Npgsql.NetTopologySuite/Npgsql.NetTopologySuite.csproj b/src/Npgsql.NetTopologySuite/Npgsql.NetTopologySuite.csproj index 214f4bd72e..ab318abc5c 100644 --- a/src/Npgsql.NetTopologySuite/Npgsql.NetTopologySuite.csproj +++ b/src/Npgsql.NetTopologySuite/Npgsql.NetTopologySuite.csproj @@ -4,8 +4,7 @@ NetTopologySuite plugin for Npgsql, allowing mapping of PostGIS geometry types to NetTopologySuite types. npgsql;postgresql;postgres;postgis;spatial;nettopologysuite;nts;ado;ado.net;database;sql README.md - net6.0 - net8.0 + net8.0 $(NoWarn);NU5104 $(NoWarn);NPG9001 diff --git a/src/Npgsql.NodaTime/Npgsql.NodaTime.csproj b/src/Npgsql.NodaTime/Npgsql.NodaTime.csproj index 3e4d826188..8bbad55db7 100644 --- a/src/Npgsql.NodaTime/Npgsql.NodaTime.csproj +++ b/src/Npgsql.NodaTime/Npgsql.NodaTime.csproj @@ -4,8 +4,7 @@ NodaTime plugin for Npgsql, allowing mapping of PostgreSQL date/time types to NodaTime types. npgsql;postgresql;postgres;nodatime;date;time;ado;ado;net;database;sql README.md - net6.0 - net8.0 + net8.0 $(NoWarn);NPG9001 diff --git a/src/Npgsql.OpenTelemetry/Npgsql.OpenTelemetry.csproj b/src/Npgsql.OpenTelemetry/Npgsql.OpenTelemetry.csproj index 7aff759251..bb1a60cc8a 100644 --- a/src/Npgsql.OpenTelemetry/Npgsql.OpenTelemetry.csproj +++ b/src/Npgsql.OpenTelemetry/Npgsql.OpenTelemetry.csproj @@ -2,8 +2,7 @@ Shay Rojansky - net6.0 - net8.0 + net8.0 npgsql;postgresql;postgres;ado;ado.net;database;sql;opentelemetry;tracing;diagnostics;instrumentation README.md diff --git a/src/Npgsql/Internal/Converters/Internal/InternalCharConverter.cs b/src/Npgsql/Internal/Converters/Internal/InternalCharConverter.cs index 5d00a26dcb..881d454d3a 100644 --- a/src/Npgsql/Internal/Converters/Internal/InternalCharConverter.cs +++ b/src/Npgsql/Internal/Converters/Internal/InternalCharConverter.cs @@ -4,10 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class InternalCharConverter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class InternalCharConverter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -15,29 +12,6 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadByte()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteByte(byte.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadByte(); - if (typeof(byte) == typeof(T)) - return (T)(object)value; - if (typeof(char) == typeof(T)) - return (T)(object)(char)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(byte) == typeof(T)) - writer.WriteByte((byte)(object)value!); - else if (typeof(char) == typeof(T)) - writer.WriteByte(checked((byte)(char)(object)value!)); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/Converters/MoneyConverter.cs b/src/Npgsql/Internal/Converters/MoneyConverter.cs index 8443acedc3..2b6c078a84 100644 --- a/src/Npgsql/Internal/Converters/MoneyConverter.cs +++ b/src/Npgsql/Internal/Converters/MoneyConverter.cs @@ -3,72 +3,17 @@ namespace Npgsql.Internal.Converters; -sealed class MoneyConverter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class MoneyConverter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { bufferRequirements = BufferRequirements.CreateFixedSize(sizeof(long)); return format is DataFormat.Binary; } + protected override T ReadCore(PgReader reader) => ConvertTo(new PgMoney(reader.ReadInt64())); protected override void WriteCore(PgWriter writer, T value) => writer.WriteInt64(ConvertFrom(value).GetValue()); - static PgMoney ConvertFrom(T value) - { -#if !NET7_0_OR_GREATER - if (typeof(short) == typeof(T)) - return new PgMoney((decimal)(short)(object)value!); - if (typeof(int) == typeof(T)) - return new PgMoney((decimal)(int)(object)value!); - if (typeof(long) == typeof(T)) - return new PgMoney((decimal)(long)(object)value!); - - if (typeof(byte) == typeof(T)) - return new PgMoney((decimal)(byte)(object)value!); - if (typeof(sbyte) == typeof(T)) - return new PgMoney((decimal)(sbyte)(object)value!); - - if (typeof(float) == typeof(T)) - return new PgMoney((decimal)(float)(object)value!); - if (typeof(double) == typeof(T)) - return new PgMoney((decimal)(double)(object)value!); - if (typeof(decimal) == typeof(T)) - return new PgMoney((decimal)(object)value!); - - throw new NotSupportedException(); -#else - return new PgMoney(decimal.CreateChecked(value)); -#endif - } - - static T ConvertTo(PgMoney money) - { -#if !NET7_0_OR_GREATER - if (typeof(short) == typeof(T)) - return (T)(object)(short)money.ToDecimal(); - if (typeof(int) == typeof(T)) - return (T)(object)(int)money.ToDecimal(); - if (typeof(long) == typeof(T)) - return (T)(object)(long)money.ToDecimal(); - - if (typeof(byte) == typeof(T)) - return (T)(object)(byte)money.ToDecimal(); - if (typeof(sbyte) == typeof(T)) - return (T)(object)(sbyte)money.ToDecimal(); - - if (typeof(float) == typeof(T)) - return (T)(object)(float)money.ToDecimal(); - if (typeof(double) == typeof(T)) - return (T)(object)(double)money.ToDecimal(); - if (typeof(decimal) == typeof(T)) - return (T)(object)money.ToDecimal(); - - throw new NotSupportedException(); -#else - return T.CreateChecked(money.ToDecimal()); -#endif - } + static PgMoney ConvertFrom(T value) => new(decimal.CreateChecked(value)); + static T ConvertTo(PgMoney money) => T.CreateChecked(money.ToDecimal()); } diff --git a/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs b/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs index 0371fb32a9..77714edf29 100644 --- a/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs +++ b/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs @@ -1,6 +1,4 @@ -#if NET8_0_OR_GREATER - -using System.Net; +using System.Net; // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; @@ -22,5 +20,3 @@ protected override IPNetwork ReadCore(PgReader reader) protected override void WriteCore(PgWriter writer, IPNetwork value) => NpgsqlInetConverter.WriteImpl(writer, (value.BaseAddress, (byte)value.PrefixLength), isCidr: true); } - -#endif diff --git a/src/Npgsql/Internal/Converters/Primitive/DoubleConverter.cs b/src/Npgsql/Internal/Converters/Primitive/DoubleConverter.cs index 74a56d06ae..8bc9caaf67 100644 --- a/src/Npgsql/Internal/Converters/Primitive/DoubleConverter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/DoubleConverter.cs @@ -4,10 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class DoubleConverter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class DoubleConverter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -15,29 +12,6 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadDouble()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteDouble(double.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadDouble(); - if (typeof(float) == typeof(T)) - return (T)(object)value; - if (typeof(double) == typeof(T)) - return (T)(object)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(float) == typeof(T)) - writer.WriteDouble((float)(object)value!); - else if (typeof(double) == typeof(T)) - writer.WriteDouble((double)(object)value!); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs b/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs index 596deedfce..a0d19a4fde 100644 --- a/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs @@ -11,35 +11,15 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer bufferRequirements = BufferRequirements.CreateFixedSize(16 * sizeof(byte)); return format is DataFormat.Binary; } + protected override Guid ReadCore(PgReader reader) - { -#if NET8_0_OR_GREATER - return new Guid(reader.ReadBytes(16).FirstSpan, bigEndian: true); -#else - return new GuidRaw - { - Data1 = reader.ReadInt32(), - Data2 = reader.ReadInt16(), - Data3 = reader.ReadInt16(), - Data4 = BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(reader.ReadInt64()) : reader.ReadInt64() - }.Value; -#endif - } + => new(reader.ReadBytes(16).FirstSpan, bigEndian: true); protected override void WriteCore(PgWriter writer, Guid value) { -#if NET8_0_OR_GREATER Span bytes = stackalloc byte[16]; value.TryWriteBytes(bytes, bigEndian: true, out _); writer.WriteBytes(bytes); -#else - var raw = new GuidRaw(value); - - writer.WriteInt32(raw.Data1); - writer.WriteInt16(raw.Data2); - writer.WriteInt16(raw.Data3); - writer.WriteInt64(BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(raw.Data4) : raw.Data4); -#endif } #if !NET8_0_OR_GREATER diff --git a/src/Npgsql/Internal/Converters/Primitive/Int2Converter.cs b/src/Npgsql/Internal/Converters/Primitive/Int2Converter.cs index e54658d925..741af9a75e 100644 --- a/src/Npgsql/Internal/Converters/Primitive/Int2Converter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/Int2Converter.cs @@ -4,67 +4,14 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class Int2Converter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class Int2Converter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { bufferRequirements = BufferRequirements.CreateFixedSize(sizeof(short)); return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER + protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadInt16()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteInt16(short.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadInt16(); - if (typeof(short) == typeof(T)) - return (T)(object)value; - if (typeof(int) == typeof(T)) - return (T)(object)(int)value; - if (typeof(long) == typeof(T)) - return (T)(object)(long)value; - - if (typeof(byte) == typeof(T)) - return (T)(object)checked((byte)value); - if (typeof(sbyte) == typeof(T)) - return (T)(object)checked((sbyte)value); - - if (typeof(float) == typeof(T)) - return (T)(object)(float)value; - if (typeof(double) == typeof(T)) - return (T)(object)(double)value; - if (typeof(decimal) == typeof(T)) - return (T)(object)(decimal)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(short) == typeof(T)) - writer.WriteInt16((short)(object)value!); - else if (typeof(int) == typeof(T)) - writer.WriteInt16(checked((short)(int)(object)value!)); - else if (typeof(long) == typeof(T)) - writer.WriteInt16(checked((short)(long)(object)value!)); - - else if (typeof(byte) == typeof(T)) - writer.WriteInt16((byte)(object)value!); - else if (typeof(sbyte) == typeof(T)) - writer.WriteInt16((sbyte)(object)value!); - - else if (typeof(float) == typeof(T)) - writer.WriteInt16(checked((short)(float)(object)value!)); - else if (typeof(double) == typeof(T)) - writer.WriteInt16(checked((short)(double)(object)value!)); - else if (typeof(decimal) == typeof(T)) - writer.WriteInt16((short)(decimal)(object)value!); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/Converters/Primitive/Int4Converter.cs b/src/Npgsql/Internal/Converters/Primitive/Int4Converter.cs index 1831ca9b1e..4327d2f2e7 100644 --- a/src/Npgsql/Internal/Converters/Primitive/Int4Converter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/Int4Converter.cs @@ -4,10 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class Int4Converter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class Int4Converter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -15,57 +12,6 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadInt32()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteInt32(int.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadInt32(); - if (typeof(short) == typeof(T)) - return (T)(object)checked((short)value); - if (typeof(int) == typeof(T)) - return (T)(object)value; - if (typeof(long) == typeof(T)) - return (T)(object)(long)value; - - if (typeof(byte) == typeof(T)) - return (T)(object)checked((byte)value); - if (typeof(sbyte) == typeof(T)) - return (T)(object)checked((sbyte)value); - - if (typeof(float) == typeof(T)) - return (T)(object)(float)value; - if (typeof(double) == typeof(T)) - return (T)(object)(double)value; - if (typeof(decimal) == typeof(T)) - return (T)(object)(decimal)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(short) == typeof(T)) - writer.WriteInt32((short)(object)value!); - else if (typeof(int) == typeof(T)) - writer.WriteInt32((int)(object)value!); - else if (typeof(long) == typeof(T)) - writer.WriteInt32(checked((int)(long)(object)value!)); - - else if (typeof(byte) == typeof(T)) - writer.WriteInt32((byte)(object)value!); - else if (typeof(sbyte) == typeof(T)) - writer.WriteInt32((sbyte)(object)value!); - - else if (typeof(float) == typeof(T)) - writer.WriteInt32(checked((int)(float)(object)value!)); - else if (typeof(double) == typeof(T)) - writer.WriteInt32(checked((int)(double)(object)value!)); - else if (typeof(decimal) == typeof(T)) - writer.WriteInt32((int)(decimal)(object)value!); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/Converters/Primitive/Int8Converter.cs b/src/Npgsql/Internal/Converters/Primitive/Int8Converter.cs index b422816244..09a54cf265 100644 --- a/src/Npgsql/Internal/Converters/Primitive/Int8Converter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/Int8Converter.cs @@ -4,10 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class Int8Converter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class Int8Converter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -15,58 +12,6 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadInt64()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteInt64(long.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadInt64(); - if (typeof(long) == typeof(T)) - return (T)(object)value; - - if (typeof(short) == typeof(T)) - return (T)(object)checked((short)value); - if (typeof(int) == typeof(T)) - return (T)(object)checked((int)value); - - if (typeof(byte) == typeof(T)) - return (T)(object)checked((byte)value); - if (typeof(sbyte) == typeof(T)) - return (T)(object)checked((sbyte)value); - - if (typeof(float) == typeof(T)) - return (T)(object)(float)value; - if (typeof(double) == typeof(T)) - return (T)(object)(double)value; - if (typeof(decimal) == typeof(T)) - return (T)(object)(decimal)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(short) == typeof(T)) - writer.WriteInt64((short)(object)value!); - else if (typeof(int) == typeof(T)) - writer.WriteInt64((int)(object)value!); - else if (typeof(long) == typeof(T)) - writer.WriteInt64((long)(object)value!); - - else if (typeof(byte) == typeof(T)) - writer.WriteInt64((byte)(object)value!); - else if (typeof(sbyte) == typeof(T)) - writer.WriteInt64((sbyte)(object)value!); - - else if (typeof(float) == typeof(T)) - writer.WriteInt64(checked((long)(float)(object)value!)); - else if (typeof(double) == typeof(T)) - writer.WriteInt64(checked((long)(double)(object)value!)); - else if (typeof(decimal) == typeof(T)) - writer.WriteInt64((long)(decimal)(object)value!); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs index c43e90a1f7..00788f99c8 100644 --- a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs +++ b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs @@ -82,12 +82,7 @@ static async ValueTask AsyncCore(PgWriter writer, BigInteger value, Cancellation static BigInteger ConvertTo(in PgNumeric numeric) => numeric.ToBigInteger(); } -sealed class DecimalNumericConverter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#else - where T : notnull -#endif +sealed class DecimalNumericConverter : PgBufferedConverter where T : INumberBase { const int StackAllocByteThreshold = 64 * sizeof(uint); @@ -129,60 +124,10 @@ protected override void WriteCore(PgWriter writer, T value) } static PgNumeric.Builder ConvertFrom(T value, Span destination) - { -#if !NET7_0_OR_GREATER - if (typeof(short) == typeof(T)) - return new PgNumeric.Builder((decimal)(short)(object)value!, destination); - if (typeof(int) == typeof(T)) - return new PgNumeric.Builder((decimal)(int)(object)value!, destination); - if (typeof(long) == typeof(T)) - return new PgNumeric.Builder((decimal)(long)(object)value!, destination); - - if (typeof(byte) == typeof(T)) - return new PgNumeric.Builder((decimal)(byte)(object)value!, destination); - if (typeof(sbyte) == typeof(T)) - return new PgNumeric.Builder((decimal)(sbyte)(object)value!, destination); - - if (typeof(float) == typeof(T)) - return new PgNumeric.Builder((decimal)(float)(object)value!, destination); - if (typeof(double) == typeof(T)) - return new PgNumeric.Builder((decimal)(double)(object)value!, destination); - if (typeof(decimal) == typeof(T)) - return new PgNumeric.Builder((decimal)(object)value!, destination); - - throw new NotSupportedException(); -#else - return new PgNumeric.Builder(decimal.CreateChecked(value), destination); -#endif - } + => new(decimal.CreateChecked(value), destination); static T ConvertTo(in PgNumeric.Builder numeric) - { -#if !NET7_0_OR_GREATER - if (typeof(short) == typeof(T)) - return (T)(object)(short)numeric.ToDecimal(); - if (typeof(int) == typeof(T)) - return (T)(object)(int)numeric.ToDecimal(); - if (typeof(long) == typeof(T)) - return (T)(object)(long)numeric.ToDecimal(); - - if (typeof(byte) == typeof(T)) - return (T)(object)(byte)numeric.ToDecimal(); - if (typeof(sbyte) == typeof(T)) - return (T)(object)(sbyte)numeric.ToDecimal(); - - if (typeof(float) == typeof(T)) - return (T)(object)(float)numeric.ToDecimal(); - if (typeof(double) == typeof(T)) - return (T)(object)(double)numeric.ToDecimal(); - if (typeof(decimal) == typeof(T)) - return (T)(object)numeric.ToDecimal(); - - throw new NotSupportedException(); -#else - return T.CreateChecked(numeric.ToDecimal()); -#endif - } + => T.CreateChecked(numeric.ToDecimal()); } static class NumericConverter diff --git a/src/Npgsql/Internal/Converters/Primitive/PgMoney.cs b/src/Npgsql/Internal/Converters/Primitive/PgMoney.cs index dc8755de1f..bddbbda648 100644 --- a/src/Npgsql/Internal/Converters/Primitive/PgMoney.cs +++ b/src/Npgsql/Internal/Converters/Primitive/PgMoney.cs @@ -51,10 +51,6 @@ static void GetDecimalBits(decimal value, Span destination, out short scal Debug.Assert(destination.Length >= DecimalBits); decimal.GetBits(value, MemoryMarshal.Cast(destination)); -#if NET7_0_OR_GREATER scale = value.Scale; -#else - scale = (byte)(destination[3] >> 16); -#endif } } diff --git a/src/Npgsql/Internal/Converters/Primitive/PgNumeric.cs b/src/Npgsql/Internal/Converters/Primitive/PgNumeric.cs index 299dd9b419..c90036d381 100644 --- a/src/Npgsql/Internal/Converters/Primitive/PgNumeric.cs +++ b/src/Npgsql/Internal/Converters/Primitive/PgNumeric.cs @@ -32,12 +32,7 @@ static void GetDecimalBits(decimal value, Span destination, out short scal Debug.Assert(destination.Length >= DecimalBits); decimal.GetBits(value, MemoryMarshal.Cast(destination)); - -#if NET7_0_OR_GREATER scale = value.Scale; -#else - scale = (byte)(destination[3] >> 16); -#endif } public static int GetDigitCount(decimal value) diff --git a/src/Npgsql/Internal/Converters/Primitive/RealConverter.cs b/src/Npgsql/Internal/Converters/Primitive/RealConverter.cs index b47e641aa5..89eeebb7fe 100644 --- a/src/Npgsql/Internal/Converters/Primitive/RealConverter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/RealConverter.cs @@ -4,10 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class RealConverter : PgBufferedConverter -#if NET7_0_OR_GREATER - where T : INumberBase -#endif +sealed class RealConverter : PgBufferedConverter where T : INumberBase { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -15,29 +12,6 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } -#if NET7_0_OR_GREATER protected override T ReadCore(PgReader reader) => T.CreateChecked(reader.ReadFloat()); protected override void WriteCore(PgWriter writer, T value) => writer.WriteFloat(float.CreateChecked(value)); -#else - protected override T ReadCore(PgReader reader) - { - var value = reader.ReadFloat(); - if (typeof(float) == typeof(T)) - return (T)(object)value; - if (typeof(double) == typeof(T)) - return (T)(object)(double)value; - - throw new NotSupportedException(); - } - - protected override void WriteCore(PgWriter writer, T value) - { - if (typeof(float) == typeof(T)) - writer.WriteFloat((float)(object)value!); - else if (typeof(double) == typeof(T)) - writer.WriteFloat((float)(double)(object)value!); - else - throw new NotSupportedException(); - } -#endif } diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 59564f5361..bfb4c2b7f1 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -135,13 +135,7 @@ async Task AuthenticateSASL(List mechanisms, string username, bool async var saltedPassword = Hi(passwd.Normalize(NormalizationForm.FormKC), saltBytes, firstServerMsg.Iteration); var clientKey = HMAC(saltedPassword, "Client Key"); - byte[] storedKey; -#if NET7_0_OR_GREATER - storedKey = SHA256.HashData(clientKey); -#else - using (var sha256 = SHA256.Create()) - storedKey = sha256.ComputeHash(clientKey); -#endif + var storedKey = SHA256.HashData(clientKey); var clientFirstMessageBare = $"n=*,r={clientNonce}"; var serverFirstMessage = $"r={firstServerMsg.Nonce},s={firstServerMsg.Salt},i={firstServerMsg.Iteration}"; var clientFinalMessageWithoutProof = $"c={cbind},r={firstServerMsg.Nonce}"; @@ -280,9 +274,6 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio throw new NpgsqlException("No password has been provided but the backend requires one (in MD5)"); byte[] result; -#if !NET7_0_OR_GREATER - using (var md5 = MD5.Create()) -#endif { // First phase var passwordBytes = NpgsqlWriteBuffer.UTF8Encoding.GetBytes(passwd); @@ -292,11 +283,7 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio usernameBytes.CopyTo(cryptBuf, passwordBytes.Length); var sb = new StringBuilder(); -#if NET7_0_OR_GREATER var hashResult = MD5.HashData(cryptBuf); -#else - var hashResult = md5.ComputeHash(cryptBuf); -#endif foreach (var b in hashResult) sb.Append(b.ToString("x2")); @@ -329,7 +316,6 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio await Flush(async, cancellationToken).ConfigureAwait(false); } -#if NET7_0_OR_GREATER internal async Task AuthenticateGSS(bool async) { var targetName = $"{KerberosServiceName}/{Host}"; @@ -360,7 +346,6 @@ internal async Task AuthenticateGSS(bool async) await Flush(async, UserCancellationToken).ConfigureAwait(false); } } -#endif async ValueTask GetPassword(string username, bool async, CancellationToken cancellationToken = default) { diff --git a/src/Npgsql/Internal/NpgsqlConnector.OldAuth.cs b/src/Npgsql/Internal/NpgsqlConnector.OldAuth.cs deleted file mode 100644 index 91aec10660..0000000000 --- a/src/Npgsql/Internal/NpgsqlConnector.OldAuth.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Npgsql.BackendMessages; -using static Npgsql.Util.Statics; - -namespace Npgsql.Internal; - - -partial class NpgsqlConnector -{ -#if !NET7_0_OR_GREATER - internal async Task AuthenticateGSS(bool async) - { - var targetName = $"{KerberosServiceName}/{Host}"; - - using var negotiateStream = new NegotiateStream(new GSSPasswordMessageStream(this), true); - try - { - if (async) - await negotiateStream.AuthenticateAsClientAsync(CredentialCache.DefaultNetworkCredentials, targetName).ConfigureAwait(false); - else - negotiateStream.AuthenticateAsClient(CredentialCache.DefaultNetworkCredentials, targetName); - } - catch (AuthenticationCompleteException) - { - return; - } - catch (IOException e) when (e.InnerException is AuthenticationCompleteException) - { - return; - } - catch (IOException e) when (e.InnerException is PostgresException) - { - throw e.InnerException; - } - - throw new NpgsqlException("NegotiateStream.AuthenticateAsClient completed unexpectedly without signaling success"); - } - - /// - /// This Stream is placed between NegotiateStream and the socket's NetworkStream (or SSLStream). It intercepts - /// traffic and performs the following operations: - /// * Outgoing messages are framed in PostgreSQL's PasswordMessage, and incoming are stripped of it. - /// * NegotiateStream frames payloads with a 5-byte header, which PostgreSQL doesn't understand. This header is - /// stripped from outgoing messages and added to incoming ones. - /// - /// - /// See https://referencesource.microsoft.com/#System/net/System/Net/_StreamFramer.cs,16417e735f0e9530,references - /// - sealed class GSSPasswordMessageStream : Stream - { - readonly NpgsqlConnector _connector; - int _leftToWrite; - int _leftToRead, _readPos; - byte[]? _readBuf; - - internal GSSPasswordMessageStream(NpgsqlConnector connector) - => _connector = connector; - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) - => Write(buffer, offset, count, true, cancellationToken); - - public override void Write(byte[] buffer, int offset, int count) - => Write(buffer, offset, count, false).GetAwaiter().GetResult(); - - async Task Write(byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken = default) - { - if (_leftToWrite == 0) - { - // We're writing the frame header, which contains the payload size. - _leftToWrite = (buffer[3] << 8) | buffer[4]; - - buffer[0] = 22; - if (buffer[1] != 1) - throw new NotSupportedException($"Received frame header major v {buffer[1]} (different from 1)"); - if (buffer[2] != 0) - throw new NotSupportedException($"Received frame header minor v {buffer[2]} (different from 0)"); - - // In case of payload data in the same buffer just after the frame header - if (count == 5) - return; - count -= 5; - offset += 5; - } - - if (count > _leftToWrite) - throw new NpgsqlException($"NegotiateStream trying to write {count} bytes but according to frame header we only have {_leftToWrite} left!"); - await _connector.WritePassword(buffer, offset, count, async, cancellationToken).ConfigureAwait(false); - await _connector.Flush(async, cancellationToken).ConfigureAwait(false); - _leftToWrite -= count; - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) - => Read(buffer, offset, count, true, cancellationToken); - - public override int Read(byte[] buffer, int offset, int count) - => Read(buffer, offset, count, false).GetAwaiter().GetResult(); - - async Task Read(byte[] buffer, int offset, int count, bool async, CancellationToken cancellationToken = default) - { - if (_leftToRead == 0) - { - var response = ExpectAny(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); - if (response.AuthRequestType == AuthenticationRequestType.Ok) - throw new AuthenticationCompleteException(); - var gssMsg = response as AuthenticationGSSContinueMessage; - if (gssMsg == null) - throw new NpgsqlException($"Received unexpected authentication request message {response.AuthRequestType}"); - _readBuf = gssMsg.AuthenticationData; - _leftToRead = gssMsg.AuthenticationData.Length; - _readPos = 0; - buffer[0] = 22; - buffer[1] = 1; - buffer[2] = 0; - buffer[3] = (byte)((_leftToRead >> 8) & 0xFF); - buffer[4] = (byte)(_leftToRead & 0xFF); - return 5; - } - - if (count > _leftToRead) - throw new NpgsqlException($"NegotiateStream trying to read {count} bytes but according to frame header we only have {_leftToRead} left!"); - count = Math.Min(count, _leftToRead); - Array.Copy(_readBuf!, _readPos, buffer, offset, count); - _leftToRead -= count; - return count; - } - - public override void Flush() { } - - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); - - public override bool CanRead => true; - public override bool CanWrite => true; - public override bool CanSeek => false; - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - } - - sealed class AuthenticationCompleteException : Exception { } -#endif -} diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 4e214d4b48..0abcb9323a 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -60,9 +60,7 @@ public sealed partial class NpgsqlConnector ProvidePasswordCallback? ProvidePasswordCallback { get; } #pragma warning restore CS0618 -#if NET7_0_OR_GREATER Action? NegotiateOptionsCallback { get; } -#endif public Encoding TextEncoding { get; private set; } = default!; @@ -913,16 +911,6 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, var host = Host; -#if !NET8_0_OR_GREATER - // If the host is a valid IP address - replace it with an empty string - // We do that because .NET uses targetHost argument to send SNI to the server - // RFC explicitly prohibits sending an IP address so some servers might fail - // This was already fixed for .NET 8 - // See #5543 for discussion - if (IPAddress.TryParse(host, out _)) - host = string.Empty; -#endif - timeout.CheckAndApply(this); var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false); diff --git a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs index 6384474cd7..04e3aa3313 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs @@ -42,12 +42,7 @@ class Resolver(Type[]? jsonbClrTypes = null, Type[]? jsonClrTypes = null, JsonSe : DynamicTypeInfoResolver, IPgTypeInfoResolver { JsonSerializerOptions? _serializerOptions = serializerOptions; - JsonSerializerOptions SerializerOptions - #if NET7_0_OR_GREATER - => _serializerOptions ??= JsonSerializerOptions.Default; - #else - => _serializerOptions ??= new(); - #endif + JsonSerializerOptions SerializerOptions => _serializerOptions ??= JsonSerializerOptions.Default; readonly Type[] _jsonbClrTypes = jsonbClrTypes ?? []; readonly Type[] _jsonClrTypes = jsonClrTypes ?? []; diff --git a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs index 0a7ebaaa1b..6bf69db4a2 100644 --- a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs @@ -51,10 +51,8 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mappings.AddStructType(DataTypeNames.Cidr, static (options, mapping, _) => mapping.CreateInfo(options, new NpgsqlCidrConverter()), isDefault: true); -#if NET8_0_OR_GREATER mappings.AddStructType(DataTypeNames.Cidr, static (options, mapping, _) => mapping.CreateInfo(options, new IPNetworkConverter())); -#endif return mappings; } diff --git a/src/Npgsql/KerberosUsernameProvider.cs b/src/Npgsql/KerberosUsernameProvider.cs index 3afb326548..591c43fd98 100644 --- a/src/Npgsql/KerberosUsernameProvider.cs +++ b/src/Npgsql/KerberosUsernameProvider.cs @@ -61,11 +61,7 @@ sealed class KerberosUsernameProvider var line = default(string); for (var i = 0; i < 2; i++) // ReSharper disable once MethodHasAsyncOverload -#if NET7_0_OR_GREATER if ((line = async ? await process.StandardOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null) -#else - if ((line = async ? await process.StandardOutput.ReadLineAsync().ConfigureAwait(false) : process.StandardOutput.ReadLine()) == null) -#endif { connectionLogger.LogDebug("Unexpected output from klist, aborting Kerberos username detection"); return null; diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index 83b804c2f6..3da49ceecc 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -136,12 +136,7 @@ internal void ReportCommandStop(long startTimestamp) if (CommandDuration.Enabled && startTimestamp > 0) { -#if NET7_0_OR_GREATER - var duration = Stopwatch.GetElapsedTime(startTimestamp); -#else - var duration = new TimeSpan((long)((Stopwatch.GetTimestamp() - startTimestamp) * StopWatchTickFrequency)); -#endif - CommandDuration.Record(duration.TotalSeconds, _poolNameTag); + CommandDuration.Record(Stopwatch.GetElapsedTime(startTimestamp).TotalSeconds, _poolNameTag); } } diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 426eb06bcd..a28f47cfbe 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -5,8 +5,7 @@ Npgsql is the open source .NET data provider for PostgreSQL. npgsql;postgresql;postgres;ado;ado.net;database;sql README.md - net6.0;net8.0 - net8.0 + net8.0 $(NoWarn);CA2017 $(NoWarn);NPG9001 $(NoWarn);NPG9002 @@ -24,12 +23,6 @@ - - - - - - diff --git a/src/Npgsql/NpgsqlBatchCommand.cs b/src/Npgsql/NpgsqlBatchCommand.cs index f25e8937b4..c1cfa3ef87 100644 --- a/src/Npgsql/NpgsqlBatchCommand.cs +++ b/src/Npgsql/NpgsqlBatchCommand.cs @@ -42,28 +42,11 @@ public override string CommandText public new NpgsqlParameterCollection Parameters => _parameters ??= []; -#if NET8_0_OR_GREATER /// - public override NpgsqlParameter CreateParameter() -#else - /// - /// Creates a new instance of a object. - /// - /// An object. - public NpgsqlParameter CreateParameter() -#endif - => new(); + public override NpgsqlParameter CreateParameter() => new(); -#if NET8_0_OR_GREATER /// - public override bool CanCreateParameter -#else - /// - /// Returns whether the method is implemented. - /// - public bool CanCreateParameter -#endif - => true; + public override bool CanCreateParameter => true; /// diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index ce6db8f843..ed434ee3c1 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -108,11 +108,8 @@ internal NpgsqlDataSource( var resolverChain, _defaultNameTranslator, ConnectionInitializer, - ConnectionInitializerAsync -#if NET7_0_OR_GREATER - ,_ -#endif - ) + ConnectionInitializerAsync, + _) = dataSourceConfig; _connectionLogger = LoggingConfiguration.ConnectionLogger; diff --git a/src/Npgsql/NpgsqlDataSourceBuilder.cs b/src/Npgsql/NpgsqlDataSourceBuilder.cs index a3b7779083..ce6926a544 100644 --- a/src/Npgsql/NpgsqlDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlDataSourceBuilder.cs @@ -364,7 +364,6 @@ public NpgsqlDataSourceBuilder UsePasswordProvider( return this; } -#if NET7_0_OR_GREATER /// /// When using Kerberos, this is a callback that allows customizing default settings for Kerberos authentication. /// @@ -380,7 +379,6 @@ public NpgsqlDataSourceBuilder UseNegotiateOptionsCallback(Action? ConnectionInitializer, - Func? ConnectionInitializerAsync -#if NET7_0_OR_GREATER - ,Action? NegotiateOptionsCallback -#endif - ); + Func? ConnectionInitializerAsync, + Action? NegotiateOptionsCallback); diff --git a/src/Npgsql/NpgsqlException.cs b/src/Npgsql/NpgsqlException.cs index 91eb84adef..89543b0b50 100644 --- a/src/Npgsql/NpgsqlException.cs +++ b/src/Npgsql/NpgsqlException.cs @@ -58,9 +58,7 @@ public override bool IsTransient /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. -#if NET8_0_OR_GREATER [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] -#endif protected internal NpgsqlException(SerializationInfo info, StreamingContext context) : base(info, context) {} #endregion diff --git a/src/Npgsql/NpgsqlFactory.cs b/src/Npgsql/NpgsqlFactory.cs index 15a1cd431e..d95e645f70 100644 --- a/src/Npgsql/NpgsqlFactory.cs +++ b/src/Npgsql/NpgsqlFactory.cs @@ -66,11 +66,9 @@ public sealed class NpgsqlFactory : DbProviderFactory, IServiceProvider /// public override DbBatchCommand CreateBatchCommand() => new NpgsqlBatchCommand(); -#if NET7_0_OR_GREATER /// public override DbDataSource CreateDataSource(string connectionString) => NpgsqlDataSource.Create(connectionString); -#endif #region IServiceProvider Members diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 376e3bd7c9..a7d9c455c5 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -38,9 +38,7 @@ public sealed class NpgsqlSlimDataSourceBuilder : INpgsqlTypeMapper Action? _clientCertificatesCallback; Action? _sslClientAuthenticationOptionsCallback; -#if NET7_0_OR_GREATER Action? _negotiateOptionsCallback; -#endif IntegratedSecurityHandler _integratedSecurityHandler = new(); @@ -853,11 +851,8 @@ _loggerFactory is null _resolverChainBuilder.Build(ConfigureResolverChain), DefaultNameTranslator, _connectionInitializer, - _connectionInitializerAsync -#if NET7_0_OR_GREATER - ,_negotiateOptionsCallback -#endif - )); + _connectionInitializerAsync, + _negotiateOptionsCallback)); } void ValidateMultiHost() diff --git a/src/Npgsql/PoolingDataSource.cs b/src/Npgsql/PoolingDataSource.cs index 46861a5c5e..c339047fb4 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 var i = 0; for (; i < MaxConnections; i++) diff --git a/src/Npgsql/PostgresException.cs b/src/Npgsql/PostgresException.cs index a157f0ab87..f941901e30 100644 --- a/src/Npgsql/PostgresException.cs +++ b/src/Npgsql/PostgresException.cs @@ -110,9 +110,7 @@ static string GetMessage(string sqlState, string messageText, int position, stri internal static PostgresException Load(NpgsqlReadBuffer buf, bool includeDetail, ILogger exceptionLogger) => new(ErrorOrNoticeMessage.Load(buf, includeDetail, exceptionLogger)); -#if NET8_0_OR_GREATER [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] -#endif internal PostgresException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Npgsql/Shims/DbDataSource.cs b/src/Npgsql/Shims/DbDataSource.cs deleted file mode 100644 index 6951d427fb..0000000000 --- a/src/Npgsql/Shims/DbDataSource.cs +++ /dev/null @@ -1,70 +0,0 @@ -#if !NET7_0_OR_GREATER - -using System.Threading; -using System.Threading.Tasks; - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (compatibility shim for old TFMs) - -// ReSharper disable once CheckNamespace -namespace System.Data.Common; - -public abstract class DbDataSource : IDisposable, IAsyncDisposable -{ - public abstract string ConnectionString { get; } - - protected abstract DbConnection CreateDbConnection(); - - // No need for an actual implementation in this compat shim - it's only implementation will be NpgsqlDataSource, which overrides this. - protected virtual DbConnection OpenDbConnection() - => throw new NotSupportedException(); - - // No need for an actual implementation in this compat shim - it's only implementation will be NpgsqlDataSource, which overrides this. - protected virtual ValueTask OpenDbConnectionAsync(CancellationToken cancellationToken = default) - => throw new NotSupportedException(); - - // No need for an actual implementation in this compat shim - it's only implementation will be NpgsqlDataSource, which overrides this. - protected virtual DbCommand CreateDbCommand(string? commandText = null) - => throw new NotSupportedException(); - - // No need for an actual implementation in this compat shim - it's only implementation will be NpgsqlDataSource, which overrides this. - protected virtual DbBatch CreateDbBatch() - => throw new NotSupportedException(); - - public DbConnection CreateConnection() - => CreateDbConnection(); - - public DbConnection OpenConnection() - => OpenDbConnection(); - - public ValueTask OpenConnectionAsync(CancellationToken cancellationToken = default) - => OpenDbConnectionAsync(cancellationToken); - - public DbCommand CreateCommand(string? commandText = null) - => CreateDbCommand(commandText); - - public DbBatch CreateBatch() - => CreateDbBatch(); - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - - Dispose(disposing: false); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - } - - protected virtual ValueTask DisposeAsyncCore() - => default; -} - -#endif \ No newline at end of file diff --git a/src/Npgsql/Shims/ExperimentalAttribute.cs b/src/Npgsql/Shims/ExperimentalAttribute.cs deleted file mode 100644 index ad6dfbf58c..0000000000 --- a/src/Npgsql/Shims/ExperimentalAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -#if !NET8_0_OR_GREATER -namespace System.Diagnostics.CodeAnalysis; - -/// Indicates that an API is experimental and it may change in the future. -[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] -public sealed class ExperimentalAttribute : Attribute -{ - /// Initializes a new instance of the class, specifying the ID that the compiler will use when reporting a use of the API the attribute applies to. - /// The ID that the compiler will use when reporting a use of the API the attribute applies to. - public ExperimentalAttribute(string diagnosticId) => DiagnosticId = diagnosticId; - - /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. - /// The unique diagnostic ID. - public string DiagnosticId { get; } - - /// Gets or sets the URL for corresponding documentation. - /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. - /// The format string that represents a URL to corresponding documentation. - public string? UrlFormat { get; set; } -} -#endif diff --git a/src/Npgsql/Shims/MemoryExtensions.cs b/src/Npgsql/Shims/MemoryExtensions.cs deleted file mode 100644 index 0da143f3c4..0000000000 --- a/src/Npgsql/Shims/MemoryExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -#if !NET7_0_OR_GREATER -using System; - -namespace Npgsql; - -static class MemoryExtensions -{ - public static int IndexOfAnyExcept(this ReadOnlySpan span, T value0, T value1) where T : IEquatable - { - for (var i = 0; i < span.Length; i++) - { - var v = span[i]; - if (!v.Equals(value0) && !v.Equals(value1)) - return i; - } - - return -1; - } -} -#endif diff --git a/src/Npgsql/Shims/StreamExtensions.cs b/src/Npgsql/Shims/StreamExtensions.cs deleted file mode 100644 index 60dbf9ca3b..0000000000 --- a/src/Npgsql/Shims/StreamExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -#if !NET7_0_OR_GREATER -using System.Threading; -using System.Threading.Tasks; - -// ReSharper disable once CheckNamespace -namespace System.IO -{ - // Helpers to read/write Span/Memory to Stream before netstandard 2.1 - static class StreamExtensions - { - public static void ReadExactly(this Stream stream, Span buffer) - { - var totalRead = 0; - while (totalRead < buffer.Length) - { - var read = stream.Read(buffer.Slice(totalRead)); - if (read is 0) - throw new EndOfStreamException(); - - totalRead += read; - } - } - - public static async ValueTask ReadExactlyAsync(this Stream stream, Memory buffer, CancellationToken cancellationToken = default) - { - var totalRead = 0; - while (totalRead < buffer.Length) - { - var read = await stream.ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); - if (read is 0) - throw new EndOfStreamException(); - - totalRead += read; - } - } - } -} -#endif diff --git a/src/Npgsql/Shims/UnreachableException.cs b/src/Npgsql/Shims/UnreachableException.cs deleted file mode 100644 index f75989df13..0000000000 --- a/src/Npgsql/Shims/UnreachableException.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if !NET7_0_OR_GREATER -namespace System.Diagnostics; - -/// -/// Exception thrown when the program executes an instruction that was thought to be unreachable. -/// -sealed class UnreachableException : Exception -{ - /// - /// Initializes a new instance of the class with the default error message. - /// - public UnreachableException() - : base("The program executed an instruction that was thought to be unreachable.") - { - } - - /// - /// Initializes a new instance of the - /// class with a specified error message. - /// - /// The error message that explains the reason for the exception. - public UnreachableException(string? message) - : base(message) - { - } - - /// - /// Initializes a new instance of the - /// class with a specified error message and a reference to the inner exception that is the cause of - /// this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - public UnreachableException(string? message, Exception? innerException) - : base(message, innerException) - { - } -} -#endif diff --git a/src/Shared/CodeAnalysis.cs b/src/Shared/CodeAnalysis.cs deleted file mode 100644 index 091b856c05..0000000000 --- a/src/Shared/CodeAnalysis.cs +++ /dev/null @@ -1,83 +0,0 @@ - -namespace System.Diagnostics.CodeAnalysis -{ -#if !NET7_0_OR_GREATER - /// - /// Indicates that the specified method requires the ability to generate new code at runtime, - /// for example through . - /// - /// - /// This allows tools to understand which methods are unsafe to call when compiling ahead of time. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] - sealed class RequiresDynamicCodeAttribute : Attribute - { - /// - /// Initializes a new instance of the class - /// with the specified message. - /// - /// - /// A message that contains information about the usage of dynamic code. - /// - public RequiresDynamicCodeAttribute(string message) - => Message = message; - - /// - /// Gets a message that contains information about the usage of dynamic code. - /// - public string Message { get; } - - /// - /// Gets or sets an optional URL that contains more information about the method, - /// why it requires dynamic code, and what options a consumer has to deal with it. - /// - public string? Url { get; set; } - } - - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - sealed class SetsRequiredMembersAttribute : Attribute - { - } - [AttributeUsageAttribute(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - sealed class UnscopedRefAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public UnscopedRefAttribute() { } - } -#endif -} - -namespace System.Runtime.CompilerServices -{ -#if !NET7_0_OR_GREATER - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - sealed class RequiredMemberAttribute : Attribute - { } - - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute - { - /// - /// The name of the compiler feature. - /// - public string FeatureName { get; } = featureName; - - /// - /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . - /// - public bool IsOptional { get; set; } - - /// - /// The used for the ref structs C# feature. - /// - public const string RefStructs = nameof(RefStructs); - - /// - /// The used for the required members C# feature. - /// - public const string RequiredMembers = nameof(RequiredMembers); - } -#endif -} diff --git a/test/Npgsql.Tests/DistributedTransactionTests.cs b/test/Npgsql.Tests/DistributedTransactionTests.cs index 5260e3daef..157e5ac112 100644 --- a/test/Npgsql.Tests/DistributedTransactionTests.cs +++ b/test/Npgsql.Tests/DistributedTransactionTests.cs @@ -1,5 +1,3 @@ -#if NET7_0_OR_GREATER - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -633,5 +631,3 @@ internal static string CreateTempTable(NpgsqlConnection conn, string columns) #endregion } - -#endif From 2f0bdd10798294fa7114c981ea773763b54356ed Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 20 Nov 2024 17:33:03 +0200 Subject: [PATCH 004/155] Some leftover cleanup for removin net6.0 (#5949) Part of #5946 --- .../Converters/Primitive/GuidUuidConverter.cs | 26 ------------------- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 16 +----------- src/Npgsql/Internal/NpgsqlConnector.cs | 3 --- .../NetworkTypeInfoResolverFactory.cs | 3 --- src/Npgsql/MetricsReporter.cs | 7 ----- src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 2 -- src/Npgsql/PostgresException.cs | 2 -- 7 files changed, 1 insertion(+), 58 deletions(-) diff --git a/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs b/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs index a0d19a4fde..18e6b0edc5 100644 --- a/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs +++ b/src/Npgsql/Internal/Converters/Primitive/GuidUuidConverter.cs @@ -21,30 +21,4 @@ protected override void WriteCore(PgWriter writer, Guid value) value.TryWriteBytes(bytes, bigEndian: true, out _); writer.WriteBytes(bytes); } - -#if !NET8_0_OR_GREATER - // The following table shows .NET GUID vs Postgres UUID (RFC 4122) layouts. - // - // Note that the first fields are converted from/to native endianness (handled by the Read* - // and Write* methods), while the last field is always read/written in big-endian format. - // - // We're reverting endianness on little endian systems to get it into big endian format. - // - // | Bits | Bytes | Name | Endianness (GUID) | Endianness (RFC 4122) | - // | ---- | ----- | ----- | ----------------- | --------------------- | - // | 32 | 4 | Data1 | Native | Big | - // | 16 | 2 | Data2 | Native | Big | - // | 16 | 2 | Data3 | Native | Big | - // | 64 | 8 | Data4 | Big | Big | - [StructLayout(LayoutKind.Explicit)] - struct GuidRaw - { - [FieldOffset(0)] public Guid Value; - [FieldOffset(0)] public int Data1; - [FieldOffset(4)] public short Data2; - [FieldOffset(6)] public short Data3; - [FieldOffset(8)] public long Data4; - public GuidRaw(Guid value) : this() => Value = value; - } -#endif } diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index bfb4c2b7f1..8771ce8e33 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -255,17 +255,7 @@ static byte[] Xor(byte[] buffer1, byte[] buffer2) return buffer1; } - static byte[] HMAC(byte[] key, string data) - { - var dataBytes = Encoding.UTF8.GetBytes(data); -#if NET7_0_OR_GREATER - return HMACSHA256.HashData(key, dataBytes); -#else - using var ih = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, key); - ih.AppendData(dataBytes); - return ih.GetHashAndReset(); -#endif - } + static byte[] HMAC(byte[] key, string data) => HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(data)); async Task AuthenticateMD5(string username, byte[] salt, bool async, CancellationToken cancellationToken = default) { @@ -298,11 +288,7 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio prehashbytes.CopyTo(cryptBuf, 0); sb = new StringBuilder("md5"); -#if NET7_0_OR_GREATER hashResult = MD5.HashData(cryptBuf); -#else - hashResult = md5.ComputeHash(cryptBuf); -#endif foreach (var b in hashResult) sb.Append(b.ToString("x2")); diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 0abcb9323a..0d6c5e23d8 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -392,10 +392,7 @@ internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn) CopyLogger = LoggingConfiguration.CopyLogger; SslClientAuthenticationOptionsCallback = dataSource.SslClientAuthenticationOptionsCallback; - -#if NET7_0_OR_GREATER NegotiateOptionsCallback = dataSource.Configuration.NegotiateOptionsCallback; -#endif State = ConnectorState.Closed; TransactionStatus = TransactionStatus.Idle; diff --git a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs index 6bf69db4a2..f9220cf8a9 100644 --- a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs @@ -78,10 +78,7 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) // cidr mappings.AddStructArrayType(DataTypeNames.Cidr); - -#if NET8_0_OR_GREATER mappings.AddStructArrayType(DataTypeNames.Cidr); -#endif return mappings; } diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index 3da49ceecc..707193e553 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -242,11 +242,4 @@ public void Dispose() Reporters.Remove(this); } } - -#if !NET7_0_OR_GREATER - const long TicksPerMicrosecond = 10; - const long TicksPerMillisecond = TicksPerMicrosecond * 1000; - const long TicksPerSecond = TicksPerMillisecond * 1000; // 10,000,000 - static readonly double StopWatchTickFrequency = (double)TicksPerSecond / Stopwatch.Frequency; -#endif } diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index a7d9c455c5..3c7de21fb7 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -341,7 +341,6 @@ public NpgsqlSlimDataSourceBuilder UsePasswordProvider( return this; } -#if NET7_0_OR_GREATER /// /// When using Kerberos, this is a callback that allows customizing default settings for Kerberos authentication. /// @@ -358,7 +357,6 @@ public NpgsqlSlimDataSourceBuilder UseNegotiateOptionsCallback(Action /// The to populate with data. /// The destination (see ) for this serialization. -#if NET8_0_OR_GREATER [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] -#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); From d606795781c48f84cc6c8877bb86339a104ea503 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 20 Nov 2024 18:20:03 +0200 Subject: [PATCH 005/155] Map date/time to DateOnly/TimeOnly by default (#5948) Closes #5328 --- .../Converters/Temporal/DateConverters.cs | 40 +++++----- .../Converters/Temporal/TimeConverters.cs | 12 +-- .../AdoTypeInfoResolverFactory.Multirange.cs | 17 ++-- .../AdoTypeInfoResolverFactory.Range.cs | 11 +-- .../AdoTypeInfoResolverFactory.cs | 13 +-- test/Npgsql.Tests/CommandBuilderTests.cs | 2 +- test/Npgsql.Tests/ExceptionTests.cs | 3 + test/Npgsql.Tests/ReaderTests.cs | 2 +- test/Npgsql.Tests/Types/DateTimeTests.cs | 79 +++++++++---------- test/Npgsql.Tests/Types/MultirangeTests.cs | 4 +- 10 files changed, 92 insertions(+), 91 deletions(-) diff --git a/src/Npgsql/Internal/Converters/Temporal/DateConverters.cs b/src/Npgsql/Internal/Converters/Temporal/DateConverters.cs index 807e2528d2..41e2cb83da 100644 --- a/src/Npgsql/Internal/Converters/Temporal/DateConverters.cs +++ b/src/Npgsql/Internal/Converters/Temporal/DateConverters.cs @@ -4,9 +4,9 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class DateTimeDateConverter(bool dateTimeInfinityConversions) : PgBufferedConverter +sealed class DateOnlyDateConverter(bool dateTimeInfinityConversions) : PgBufferedConverter { - static readonly DateTime BaseValue = new(2000, 1, 1, 0, 0, 0); + static readonly DateOnly BaseValue = new(2000, 1, 1); public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -14,42 +14,42 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } - protected override DateTime ReadCore(PgReader reader) + protected override DateOnly ReadCore(PgReader reader) => reader.ReadInt32() switch { int.MaxValue => dateTimeInfinityConversions - ? DateTime.MaxValue + ? DateOnly.MaxValue : throw new InvalidCastException(NpgsqlStrings.CannotReadInfinityValue), int.MinValue => dateTimeInfinityConversions - ? DateTime.MinValue + ? DateOnly.MinValue : throw new InvalidCastException(NpgsqlStrings.CannotReadInfinityValue), - var value => BaseValue + TimeSpan.FromDays(value) + var value => BaseValue.AddDays(value) }; - protected override void WriteCore(PgWriter writer, DateTime value) + protected override void WriteCore(PgWriter writer, DateOnly value) { if (dateTimeInfinityConversions) { - if (value == DateTime.MaxValue) + if (value == DateOnly.MaxValue) { writer.WriteInt32(int.MaxValue); return; } - if (value == DateTime.MinValue) + if (value == DateOnly.MinValue) { writer.WriteInt32(int.MinValue); return; } } - writer.WriteInt32((value.Date - BaseValue).Days); + writer.WriteInt32(value.DayNumber - BaseValue.DayNumber); } } -sealed class DateOnlyDateConverter(bool dateTimeInfinityConversions) : PgBufferedConverter +sealed class DateTimeDateConverter(bool dateTimeInfinityConversions) : PgBufferedConverter { - static readonly DateOnly BaseValue = new(2000, 1, 1); + static readonly DateTime BaseValue = new(2000, 1, 1, 0, 0, 0); public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { @@ -57,35 +57,35 @@ public override bool CanConvert(DataFormat format, out BufferRequirements buffer return format is DataFormat.Binary; } - protected override DateOnly ReadCore(PgReader reader) + protected override DateTime ReadCore(PgReader reader) => reader.ReadInt32() switch { int.MaxValue => dateTimeInfinityConversions - ? DateOnly.MaxValue + ? DateTime.MaxValue : throw new InvalidCastException(NpgsqlStrings.CannotReadInfinityValue), int.MinValue => dateTimeInfinityConversions - ? DateOnly.MinValue + ? DateTime.MinValue : throw new InvalidCastException(NpgsqlStrings.CannotReadInfinityValue), - var value => BaseValue.AddDays(value) + var value => BaseValue + TimeSpan.FromDays(value) }; - protected override void WriteCore(PgWriter writer, DateOnly value) + protected override void WriteCore(PgWriter writer, DateTime value) { if (dateTimeInfinityConversions) { - if (value == DateOnly.MaxValue) + if (value == DateTime.MaxValue) { writer.WriteInt32(int.MaxValue); return; } - if (value == DateOnly.MinValue) + if (value == DateTime.MinValue) { writer.WriteInt32(int.MinValue); return; } } - writer.WriteInt32(value.DayNumber - BaseValue.DayNumber); + writer.WriteInt32((value.Date - BaseValue).Days); } } diff --git a/src/Npgsql/Internal/Converters/Temporal/TimeConverters.cs b/src/Npgsql/Internal/Converters/Temporal/TimeConverters.cs index b93a878032..09385712bf 100644 --- a/src/Npgsql/Internal/Converters/Temporal/TimeConverters.cs +++ b/src/Npgsql/Internal/Converters/Temporal/TimeConverters.cs @@ -3,26 +3,26 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; -sealed class TimeSpanTimeConverter : PgBufferedConverter +sealed class TimeOnlyTimeConverter : PgBufferedConverter { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { bufferRequirements = BufferRequirements.CreateFixedSize(sizeof(long)); return format is DataFormat.Binary; } - protected override TimeSpan ReadCore(PgReader reader) => new(reader.ReadInt64() * 10); - protected override void WriteCore(PgWriter writer, TimeSpan value) => writer.WriteInt64(value.Ticks / 10); + protected override TimeOnly ReadCore(PgReader reader) => new(reader.ReadInt64() * 10); + protected override void WriteCore(PgWriter writer, TimeOnly value) => writer.WriteInt64(value.Ticks / 10); } -sealed class TimeOnlyTimeConverter : PgBufferedConverter +sealed class TimeSpanTimeConverter : PgBufferedConverter { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) { bufferRequirements = BufferRequirements.CreateFixedSize(sizeof(long)); return format is DataFormat.Binary; } - protected override TimeOnly ReadCore(PgReader reader) => new(reader.ReadInt64() * 10); - protected override void WriteCore(PgWriter writer, TimeOnly value) => writer.WriteInt64(value.Ticks / 10); + protected override TimeSpan ReadCore(PgReader reader) => new(reader.ReadInt64() * 10); + protected override void WriteCore(PgWriter writer, TimeSpan value) => writer.WriteInt64(value.Ticks / 10); } sealed class DateTimeOffsetTimeTzConverter : PgBufferedConverter diff --git a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Multirange.cs b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Multirange.cs index f76ed3a457..3d82ab03f1 100644 --- a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Multirange.cs +++ b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Multirange.cs @@ -159,24 +159,23 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) CreateListMultirangeConverter(CreateRangeConverter(new Int8Converter(), options), options))); // datemultirange - mappings.AddType[]>(DataTypeNames.DateMultirange, - static (options, mapping, _) => - mapping.CreateInfo(options, CreateArrayMultirangeConverter( - CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options), options)), - isDefault: true); - mappings.AddType>>(DataTypeNames.DateMultirange, - static (options, mapping, _) => - mapping.CreateInfo(options, CreateListMultirangeConverter( - CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options), options))); mappings.AddType[]>(DataTypeNames.DateMultirange, static (options, mapping, _) => mapping.CreateInfo(options, CreateArrayMultirangeConverter( CreateRangeConverter(new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions), options), options)), isDefault: true); + mappings.AddType[]>(DataTypeNames.DateMultirange, + static (options, mapping, _) => + mapping.CreateInfo(options, CreateArrayMultirangeConverter( + CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options), options))); mappings.AddType>>(DataTypeNames.DateMultirange, static (options, mapping, _) => mapping.CreateInfo(options, CreateListMultirangeConverter( CreateRangeConverter(new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions), options), options))); + mappings.AddType>>(DataTypeNames.DateMultirange, + static (options, mapping, _) => + mapping.CreateInfo(options, CreateListMultirangeConverter( + CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options), options))); return mappings; } diff --git a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Range.cs b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Range.cs index 74a9028423..17ba8c3c33 100644 --- a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Range.cs +++ b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.Range.cs @@ -87,15 +87,16 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, CreateRangeConverter(new Int8Converter(), options))); // daterange + mappings.AddStructType>(DataTypeNames.DateRange, + static (options, mapping, _) => + mapping.CreateInfo(options, + CreateRangeConverter(new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions), options)), + isDefault: true); mappings.AddStructType>(DataTypeNames.DateRange, static (options, mapping, _) => mapping.CreateInfo(options, - CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options)), - isDefault: true); + CreateRangeConverter(new DateTimeDateConverter(options.EnableDateTimeInfinityConversions), options))); mappings.AddStructType>(DataTypeNames.DateRange, static (options, mapping, _) => mapping.CreateInfo(options, CreateRangeConverter(new Int4Converter(), options))); - mappings.AddStructType>(DataTypeNames.DateRange, - static (options, mapping, _) => - mapping.CreateInfo(options, CreateRangeConverter(new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions), options))); return mappings; } diff --git a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs index b2a39db34b..db350e2fc9 100644 --- a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs @@ -230,14 +230,15 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, new Int8Converter())); // Date + mappings.AddStructType(DataTypeNames.Date, + static (options, mapping, _) => + mapping.CreateInfo(options, new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions)), isDefault: true); mappings.AddStructType(DataTypeNames.Date, static (options, mapping, _) => mapping.CreateInfo(options, new DateTimeDateConverter(options.EnableDateTimeInfinityConversions)), MatchRequirement.DataTypeName); mappings.AddStructType(DataTypeNames.Date, static (options, mapping, _) => mapping.CreateInfo(options, new Int4Converter())); - mappings.AddStructType(DataTypeNames.Date, - static (options, mapping, _) => mapping.CreateInfo(options, new DateOnlyDateConverter(options.EnableDateTimeInfinityConversions))); // Interval mappings.AddStructType(DataTypeNames.Interval, @@ -246,12 +247,12 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, new NpgsqlIntervalConverter())); // Time + mappings.AddStructType(DataTypeNames.Time, + static (options, mapping, _) => mapping.CreateInfo(options, new TimeOnlyTimeConverter()), isDefault: true); mappings.AddStructType(DataTypeNames.Time, - static (options, mapping, _) => mapping.CreateInfo(options, new TimeSpanTimeConverter()), isDefault: true); + static (options, mapping, _) => mapping.CreateInfo(options, new TimeSpanTimeConverter())); mappings.AddStructType(DataTypeNames.Time, static (options, mapping, _) => mapping.CreateInfo(options, new Int8Converter())); - mappings.AddStructType(DataTypeNames.Time, - static (options, mapping, _) => mapping.CreateInfo(options, new TimeOnlyTimeConverter())); // TimeTz mappings.AddStructType(DataTypeNames.TimeTz, @@ -446,9 +447,9 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mappings.AddStructArrayType(DataTypeNames.TimestampTz); // Date + mappings.AddStructArrayType(DataTypeNames.Date); mappings.AddStructArrayType(DataTypeNames.Date); mappings.AddStructArrayType(DataTypeNames.Date); - mappings.AddStructArrayType(DataTypeNames.Date); // Interval mappings.AddStructArrayType(DataTypeNames.Interval); diff --git a/test/Npgsql.Tests/CommandBuilderTests.cs b/test/Npgsql.Tests/CommandBuilderTests.cs index e917b7f6b3..a9fe980c5b 100644 --- a/test/Npgsql.Tests/CommandBuilderTests.cs +++ b/test/Npgsql.Tests/CommandBuilderTests.cs @@ -341,7 +341,7 @@ PRIMARY KEY (Cod) Assert.That(row[0], Is.EqualTo("key1")); Assert.That(row[1], Is.EqualTo("description")); - Assert.That(row[2], Is.EqualTo(new DateTime(2018, 7, 3))); + Assert.That(row[2], Is.EqualTo(new DateOnly(2018, 7, 3))); Assert.That(row[3], Is.EqualTo(new DateTime(2018, 7, 3, 7, 2, 0))); Assert.That(row[4], Is.EqualTo(123)); Assert.That(row[5], Is.EqualTo(123.4)); diff --git a/test/Npgsql.Tests/ExceptionTests.cs b/test/Npgsql.Tests/ExceptionTests.cs index ac87ef2b0e..21d83ff9fb 100644 --- a/test/Npgsql.Tests/ExceptionTests.cs +++ b/test/Npgsql.Tests/ExceptionTests.cs @@ -250,6 +250,8 @@ PostgresException CreateWithSqlState(string sqlState) #pragma warning disable SYSLIB0011 #pragma warning disable SYSLIB0050 + +#if !NET9_0_OR_GREATER // BinaryFormatter serialization and deserialization have been removed. See https://aka.ms/binaryformatter for more information. [Test] public void Serialization() { @@ -283,6 +285,7 @@ public void Serialization() Assert.That(expected.Line, Is.EqualTo(actual.Line)); Assert.That(expected.Routine, Is.EqualTo(actual.Routine)); } +#endif SerializationInfo CreateSerializationInfo() => new(typeof(PostgresException), new FormatterConverter()); #pragma warning restore SYSLIB0011 diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index 7ee1aa6e11..b46342153f 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -475,7 +475,7 @@ public async Task GetValues() dr.Read(); var values = new object[4]; Assert.That(dr.GetValues(values), Is.EqualTo(3)); - Assert.That(values, Is.EqualTo(new object?[] { "hello", 1, new DateTime(2014, 1, 1), null })); + Assert.That(values, Is.EqualTo(new object?[] { "hello", 1, new DateOnly(2014, 1, 1), null })); } using (var dr = await command.ExecuteReaderAsync(Behavior)) { diff --git a/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index 815514031a..078693bf96 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -13,9 +13,13 @@ public class DateTimeTests : TestBase { #region Date + [Test] + public Task Date_as_DateOnly() + => AssertType(new DateOnly(2020, 10, 1), "2020-10-01", "date", NpgsqlDbType.Date, DbType.Date); + [Test] public Task Date_as_DateTime() - => AssertType(new DateTime(2020, 10, 1), "2020-10-01", "date", NpgsqlDbType.Date, DbType.Date, isDefaultForWriting: false); + => AssertType(new DateTime(2020, 10, 1), "2020-10-01", "date", NpgsqlDbType.Date, DbType.Date, isDefault: false); [Test] public Task Date_as_DateTime_with_date_and_time_before_2000() @@ -26,37 +30,6 @@ public Task Date_as_DateTime_with_date_and_time_before_2000() public Task Date_as_int() => AssertType(7579, "2020-10-01", "date", NpgsqlDbType.Date, DbType.Date, isDefault: false); - [Test] - public Task Daterange_as_NpgsqlRange_of_DateTime() - => AssertType( - new NpgsqlRange(new(2002, 3, 4), true, new(2002, 3, 6), false), - "[2002-03-04,2002-03-06)", - "daterange", - NpgsqlDbType.DateRange, - isDefaultForWriting: false); - - [Test] - public async Task Datemultirange_as_array_of_NpgsqlRange_of_DateTime() - { - await using var conn = await OpenConnectionAsync(); - MinimumPgVersion(conn, "14.0", "Multirange types were introduced in PostgreSQL 14"); - - await AssertType( - new[] - { - new NpgsqlRange(new(2002, 3, 4), true, new(2002, 3, 6), false), - new NpgsqlRange(new(2002, 3, 8), true, new(2002, 3, 11), false) - }, - "{[2002-03-04,2002-03-06),[2002-03-08,2002-03-11)}", - "datemultirange", - NpgsqlDbType.DateMultirange, - isDefaultForWriting: false); - } - - [Test] - public Task Date_as_DateOnly() - => AssertType(new DateOnly(2020, 10, 1), "2020-10-01", "date", NpgsqlDbType.Date, DbType.Date, isDefaultForReading: false); - [Test] public Task Daterange_as_NpgsqlRange_of_DateOnly() => AssertType( @@ -64,7 +37,6 @@ public Task Daterange_as_NpgsqlRange_of_DateOnly() "[2002-03-04,2002-03-06)", "daterange", NpgsqlDbType.DateRange, - isDefaultForReading: false, skipArrayCheck: true); // NpgsqlRange[] is mapped to multirange by default, not array; test separately [Test] @@ -78,6 +50,15 @@ public Task Daterange_array_as_NpgsqlRange_of_DateOnly_array() """{"[2002-03-04,2002-03-06)","[2002-03-08,2002-03-09)"}""", "daterange[]", NpgsqlDbType.DateRange | NpgsqlDbType.Array, + isDefaultForWriting: false); + + [Test] + public Task Daterange_as_NpgsqlRange_of_DateTime() + => AssertType( + new NpgsqlRange(new(2002, 3, 4), true, new(2002, 3, 6), false), + "[2002-03-04,2002-03-06)", + "daterange", + NpgsqlDbType.DateRange, isDefault: false); [Test] @@ -94,8 +75,25 @@ await AssertType( }, "{[2002-03-04,2002-03-06),[2002-03-08,2002-03-11)}", "datemultirange", + NpgsqlDbType.DateMultirange); + } + + [Test] + public async Task Datemultirange_as_array_of_NpgsqlRange_of_DateTime() + { + await using var conn = await OpenConnectionAsync(); + MinimumPgVersion(conn, "14.0", "Multirange types were introduced in PostgreSQL 14"); + + await AssertType( + new[] + { + new NpgsqlRange(new(2002, 3, 4), true, new(2002, 3, 6), false), + new NpgsqlRange(new(2002, 3, 8), true, new(2002, 3, 11), false) + }, + "{[2002-03-04,2002-03-06),[2002-03-08,2002-03-11)}", + "datemultirange", NpgsqlDbType.DateMultirange, - isDefaultForReading: false); + isDefault: false); } #endregion @@ -103,24 +101,23 @@ await AssertType( #region Time [Test] - public Task Time_as_TimeSpan() + public Task Time_as_TimeOnly() => AssertType( - new TimeSpan(0, 10, 45, 34, 500), + new TimeOnly(10, 45, 34, 500), "10:45:34.5", "time without time zone", NpgsqlDbType.Time, - DbType.Time, - isDefaultForWriting: false); + DbType.Time); [Test] - public Task Time_as_TimeOnly() + public Task Time_as_TimeSpan() => AssertType( - new TimeOnly(10, 45, 34, 500), + new TimeSpan(0, 10, 45, 34, 500), "10:45:34.5", "time without time zone", NpgsqlDbType.Time, DbType.Time, - isDefaultForReading: false); + isDefault: false); #endregion diff --git a/test/Npgsql.Tests/Types/MultirangeTests.cs b/test/Npgsql.Tests/Types/MultirangeTests.cs index cf9cdcd8a9..86bebb1b67 100644 --- a/test/Npgsql.Tests/Types/MultirangeTests.cs +++ b/test/Npgsql.Tests/Types/MultirangeTests.cs @@ -47,12 +47,12 @@ public class MultirangeTests : TestBase // daterange new TestCaseData( - new NpgsqlRange[] + new NpgsqlRange[] { new(new(2020, 1, 1), true, false, new(2020, 1, 5), false, false), new(new(2020, 1, 10), true, false, default, false, true) }, - "{[2020-01-01,2020-01-05),[2020-01-10,)}", "datemultirange", NpgsqlDbType.DateMultirange, true, false, default(NpgsqlRange)) + "{[2020-01-01,2020-01-05),[2020-01-10,)}", "datemultirange", NpgsqlDbType.DateMultirange, true, false, default(NpgsqlRange)) .SetName("DateTime DateMultirange"), // tsmultirange From a1f1022a7103e069c48c3450e45d19a642d6995c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 20 Nov 2024 18:28:27 +0200 Subject: [PATCH 006/155] Make the cidr<->IPNetwork mapping the default (#5950) Closes #5891 --- .../Converters/Networking/NpgsqlCidrConverter.cs | 2 ++ .../NetworkTypeInfoResolverFactory.cs | 14 +++++++++----- src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs | 1 + test/Npgsql.Tests/Types/NetworkTypeTests.cs | 14 +++++++------- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Npgsql/Internal/Converters/Networking/NpgsqlCidrConverter.cs b/src/Npgsql/Internal/Converters/Networking/NpgsqlCidrConverter.cs index c3cd43c227..451fab4959 100644 --- a/src/Npgsql/Internal/Converters/Networking/NpgsqlCidrConverter.cs +++ b/src/Npgsql/Internal/Converters/Networking/NpgsqlCidrConverter.cs @@ -3,6 +3,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; +#pragma warning disable CS0618 // NpgsqlCidr is obsolete sealed class NpgsqlCidrConverter : PgBufferedConverter { public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) @@ -20,3 +21,4 @@ protected override NpgsqlCidr ReadCore(PgReader reader) protected override void WriteCore(PgWriter writer, NpgsqlCidr value) => NpgsqlInetConverter.WriteImpl(writer, (value.Address, value.Netmask), isCidr: true); } +#pragma warning restore CS0618 diff --git a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs index f9220cf8a9..5eb072c1c9 100644 --- a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs @@ -48,11 +48,13 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, new NpgsqlInetConverter())); // cidr - mappings.AddStructType(DataTypeNames.Cidr, - static (options, mapping, _) => mapping.CreateInfo(options, new NpgsqlCidrConverter()), isDefault: true); - mappings.AddStructType(DataTypeNames.Cidr, - static (options, mapping, _) => mapping.CreateInfo(options, new IPNetworkConverter())); + static (options, mapping, _) => mapping.CreateInfo(options, new IPNetworkConverter()), isDefault: true); + +#pragma warning disable CS0618 // NpgsqlCidr is obsolete + mappings.AddStructType(DataTypeNames.Cidr, + static (options, mapping, _) => mapping.CreateInfo(options, new NpgsqlCidrConverter())); +#pragma warning restore CS0618 return mappings; } @@ -77,8 +79,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mappings.AddStructArrayType(DataTypeNames.Inet); // cidr - mappings.AddStructArrayType(DataTypeNames.Cidr); mappings.AddStructArrayType(DataTypeNames.Cidr); +#pragma warning disable CS0618 // NpgsqlCidr is obsolete + mappings.AddStructArrayType(DataTypeNames.Cidr); +#pragma warning restore CS0618 return mappings; } diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs index 493be99dea..7d5eedf7af 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs @@ -488,6 +488,7 @@ static void CheckAddressFamily(IPAddress address) /// /// https://www.postgresql.org/docs/current/static/datatype-net-types.html /// +[Obsolete("Use .NET IPNetwork instead of NpgsqlCidr to map to PostgreSQL cidr")] public readonly record struct NpgsqlCidr { public IPAddress Address { get; } diff --git a/test/Npgsql.Tests/Types/NetworkTypeTests.cs b/test/Npgsql.Tests/Types/NetworkTypeTests.cs index e24446f136..b3f0b221e6 100644 --- a/test/Npgsql.Tests/Types/NetworkTypeTests.cs +++ b/test/Npgsql.Tests/Types/NetworkTypeTests.cs @@ -53,23 +53,23 @@ public Task IPAddress_Any() => AssertTypeWrite(IPAddress.Any, "0.0.0.0/32", "inet", NpgsqlDbType.Inet, skipArrayCheck: true); [Test] - public Task Cidr() + public Task IPNetwork_as_cidr() => AssertType( - new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), netmask: 24), + new IPNetwork(IPAddress.Parse("192.168.1.0"), 24), "192.168.1.0/24", "cidr", - NpgsqlDbType.Cidr, - isDefaultForWriting: false); + NpgsqlDbType.Cidr); +#pragma warning disable CS0618 // NpgsqlCidr is obsolete [Test] - public Task IPNetwork_as_cidr() + public Task NpgsqlCidr_as_Cidr() => AssertType( - new IPNetwork(IPAddress.Parse("192.168.1.0"), 24), + new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), netmask: 24), "192.168.1.0/24", "cidr", NpgsqlDbType.Cidr, - isDefaultForWriting: false, isDefaultForReading: false); +#pragma warning restore CS0618 [Test] public Task Inet_v4_as_NpgsqlInet() From 2b013ed5216c49ed4257c6bee5a994885a1bd66f Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 20 Nov 2024 20:29:31 +0300 Subject: [PATCH 007/155] Fix connecting with VerifyCA and VerifyFull (#5944) Fixes #5942 --- .build/ca.crt | 31 ++++++++++ .build/server.crt | 46 +++++++++------ .build/server.key | 79 +++++++++++++++++--------- .github/workflows/build.yml | 20 ++++--- src/Npgsql/Internal/NpgsqlConnector.cs | 2 +- test/Npgsql.Tests/Npgsql.Tests.csproj | 5 ++ test/Npgsql.Tests/SecurityTests.cs | 41 +++++++++++++ 7 files changed, 169 insertions(+), 55 deletions(-) create mode 100644 .build/ca.crt diff --git a/.build/ca.crt b/.build/ca.crt new file mode 100644 index 0000000000..e5a4081a02 --- /dev/null +++ b/.build/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUB/AJgMX+fmeXvBOUWW7WR+XKZ6AwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDExMjAwNDExMjFaFw0zNDEx +MTgwNDExMjFaMEUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC8A5+//15VxRCxpHzl7srYx6uWQi1/7q5VFWFZab+7 +82PLr3pV/zMjSMEPBZdq46NWWNnXIFoFHd5MFnN4fNIQ1GIEsTF0kYy142qllnp3 +vLBVBu24n4dsmI8ygl8+1PuGwk45Mz+vOL+RjNIo6ra9yJzYFnZOGCqlt0kWkCau +HR/43ms0vhKq8FaDXPdVXn9Z3EZScxRKQwlfAKOUxLQ8dVkzvRuAm0PF74afRYfg +xiGIX8msFYKzGnWb7ezcag125iEqg+xSplo6QK6vaNURlKwYQ8ZRKz1Hk1oIB4t1 +iEJL2d4nzgTkh/jlVjtTXo6cw96WT9NBT0Rg6JR4PJySlhY+ZwLi6VAxQZ8GyJo4 +YTvx1K3vhXeokKjFTxUtZdx1blX5vCBXv9LCxnjAsBCTRzE425x6UP1gp721gHGW +sqopvkUgN9vk8oigyWLeGvwsBwFTFnY672iCYXhFHs2oKTIX8yo+A2xRr8tewb9C +IsqJSC6JkLs5zbVwKdgVx1H21Uwvi7XjKir9pPp/ks12r9GNMmWc265PK1kCqCHa +oHfgzYMVVFQ3CfYbeeA8/aVf770AfC/1v+VtMse8DEqyep5q0OzOXtWIQlahYiyA +FLTzCBqcHUuRZtS4gEhOk6/Pk1HP3faUC1xGgxO5c/pd7SVMfs+Z58WJbYGFcAlC ++QIDAQABo1MwUTAdBgNVHQ4EFgQUBeKaoc7AMURxdajJ+CF8YrUsdFgwHwYDVR0j +BBgwFoAUBeKaoc7AMURxdajJ+CF8YrUsdFgwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAGGpFZm0c36Eh5E8QiAg8+8U22Ao+YoF6nJnIlc/ri1pt +J5zXRM2DbCCR9uN5yckmCNIJ4PZO49QBflYGPAkF+Vd0RJYoA4k1Cq+eYcJBWtXl +ESJxeg1QAKAZ4XSasOIijebWlPIZxPGOy8HquKNMDQIm8a7g5zSE4UNJPVY3y9on +zJT7ZhntIwuM8IP6h6gotJfxBHJRWNe/g0zVITQ7vHnxSpobLbuKfY21GLl6clgI +WsePKWWo/mZYquqZz72KBUJ66YX4X7nJCvZs1sLgMnXh87n9hsxAdFlRgLuQ4ztp +mwQbDZ90mJFQLprI4rfyamuloIgOcn05yXfklRAI2P8L2/yf5xNAy+ii0OHRiMVv +jnYUet8Bca1orh7OQ9ol1XTBoCI1gknrdG5Y2IQvQhWLiS5AjIwwQYwjkSFXELtF +X8v9Fv758RA9CFlQDnsp9awNjdLss/TdH6+dNYQfTNGigIPM6oCk5nrcQqF/533W +z2WM0LNHAiQlEn0X38D0wCuRwIVzPG/AFyfsf50vSlH81/uzpyR5q3SJA8OKiCV1 +/OiW7Jv7pOtwqFjxR+m31TqaPM6PLrdasP/CNKSvGuJmtaHK4Wkc3YU9dbtQffzB +MUFwhi233gvE+nSEixse2KlzsrBVZIdz16bZXaAd20JQdq9Hceku2uVgfN1fycI= +-----END CERTIFICATE----- diff --git a/.build/server.crt b/.build/server.crt index d161ab2652..5a2bfc7b01 100644 --- a/.build/server.crt +++ b/.build/server.crt @@ -1,20 +1,30 @@ -----BEGIN CERTIFICATE----- -MIIDUjCCAjoCFAwuj6RwuZSjCGYHja8m9tbr3nFeMA0GCSqGSIb3DQEBCwUAMGgx -EzARBgNVBAoTCk15IENvbXBhbnkxCzAJBgNVBAsTAklUMRAwDgYDVQQHEwdNeSBU -b3duMQ8wDQYDVQQIEwZNb3Njb3cxCzAJBgNVBAYTAlJVMRQwEgYDVQQDEwtsb2Nh -bGhvc3RDQTAeFw0yMTA0MTAxMzA0MDBaFw0yMjA0MTAxMzA0MDBaMGMxEzARBgNV -BAoTCk15IENvbXBhbnkxCzAJBgNVBAsTAklUMRAwDgYDVQQHEwdNeSBUb3duMQ8w -DQYDVQQIEwZNb3Njb3cxCzAJBgNVBAYTAlJVMQ8wDQYDVQQDEwZzZXJ2ZXIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8LoQbo2DFwC17gZwJ8xrPKHGX -UKxoo5UcyZ3/2zZ006TYkswssejKksuiICTMI89OD8n55pNTZkXPUH7oR2oIyxTY -SiWPiNzbEh0FOxH9Kh5gmajqM/4X44OaprmyQ56m4Y2LZO2nZ9hHoe+ZRoan3+pa -g8weOM/n/wYuXZtdElOxNsB8pg09K4gevHVaLaSBCEeQfHev51vClFdN3+orBi/r -hnQF3vdw7oMT1JSH75Ray51wRaypLIslAc2DcPFTCQJMmXXMTcAcxmjAVUGrfY+d -sSCdXnOZtd7yk+0X0bVGKLBkCTOP7QpmfOVu9bOhscDiK5EoAaDKqdHSMUfhAgMB -AAEwDQYJKoZIhvcNAQELBQADggEBAKCo2Y1uKbudA8JpV6yo35tc7Z6n03++BAdq -egUBKOiE4ze7xQ7lmlt572ptqXlU/8JuPWa2Qb/wGksR0HpVPTAeU3pbXz1dcCXC -A9wCtSxapjyCYbkDrDl2FQuK0OfJi0q71JZU66D58Qu0l45nWON30to9dSiw3zPw -Rdk7X86GHYIBHKsj7mjiy1v8jH1sXeWvThOmU6+rv8UY8VuJiu4MQDdYa0Y5KFh/ -OL3tVsi7zoNu2OXY1cTKuUpKMQPbO+WSdelYromYK2OAXaNqnC27GegPqvCFWJ2I -9NZuXYj3X+j0ydZSKVjDgCda8H68olBnO0zh44XirCBef7uTVLw= +MIIFJTCCAw0CFAKjNOhsMTYUuQngy2k291XuKOGGMA0GCSqGSIb3DQEBCwUAMEUx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQxMTIwMDQxOTE0WhcNMjkxMTE5MDQx +OTE0WjBZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE +CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3Qw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDlGT9vXb93yoM1YT0GAxJI +B6/2ExUrdprd049oMVZa4Km0nqwN/xjVvQRIWozmbpvps0mCkFM1ZyL1iqZFwiJG +WcQvvIffFM1qKRMOSTLNPCbM9mfvRKsCU9gjgatdhy8xUZhz7uFGMGADnZdlNMYW +GgzMVZo0EyW7Z2QJ+ZCl8wW5IT4iswZWrJsNZU/g7HaNBrXiidDihkmQ8Kt32R0U +nqJeXMHwkQLxddmcGdDmVCKsAEUu3NcvPeAlSJsNHfGDRsf9fImRqZCsgwI8dJtA +ke/luMTttQ34aADFTmTbVk4ngVhCxgBkJ6FUDFJcp3t3nFssiisNon9k5FwtJ3hl +e/QGM9IRdBvGVcOnZZuXXK2lLtakj5UWUik2xWA0hjX+DsFo7TPwKgZy4zmWCRob +W1e1NX52bqYFWZUKYLqbizllOd98o3yed58PhbF1/IuVEuOoiKu7rNdNgzr8vgRP +pWHQNXp3maCcZq2kWybADU2LQNUKAZLSw3nClcX8QVRAfvf8IyDZ/280EYRGu99V +qLqDPLa1+3CNAb93J1ONvVjKgJwQQWy4dYFLHTYdBzXV5SOpH8YHL/1IHs9W5k28 +BdwbeMtJnOaV8rqiA6Xd4Xem111AMAigHExxG3kpSnAq6jiOX0+2V++f7qAunuC6 +B/oJATXLCbBQILr0ARtKuQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCn6R2fvxfs +R7nN9g6bVNJXkJrDJ+O1suVD0tkZzxZAIAFdhKnSFocJph1bC6bSZEQkhG+0WtfU +DU7m19VDHpZWZ+8LygIVikIkvj47v1/yl7TgwkhNAKXXxl6bF/AEevMUZoxT3r8S +UBFURp8QduSQ7sbDRB9qR1EWPjAXgnedzLSGkt5E6VKuVRwsTjv7QUTV8RCbOl9b +YHtTX3dtvr3PeAB5M3B6qrbpniqJfPxUt658UKrDGFr1MuZZ8ONYpdiGH8uGXZhs +9BBjp0g0xWha9LYDYRpqzlC1hqV0J/9jz9QdS9HHPsqa8PvB/YwaDGQm/RSRMUbU +x0wip0me45WU5pLD1djEGQBlxCGgQXIJsebzipdUsayA4MgY3s2lBj2qsPOqyNoP +dFohMm2+Ypi8UAjEbeGY4XsCODLeCvPx24HyjJUORm9uuPCunSBhtgiEBTJrNwHL +F7T1+/g9gVSwCsz4MqceO7IooJ2omSpwk7xrzocccFb1HGR/tE9GxRLNHiyTfx9s +FN9SNOih5DCcOFOiw0vF1qKHk6CAJ0UCBzVWl3YO9OgnFX4FbRYHd3PduWR+fSkd +icBs2AiOKPbOU8yXR8CE6uZiDoN6A27KOE07adZEWBMwd4us7uBHGgnqqYuwPI3d +nqC8srMQ07fw8HyXn7ojPxXyCk+2d6zVgA== -----END CERTIFICATE----- diff --git a/.build/server.key b/.build/server.key index b6dd15913f..f2a7e607b2 100644 --- a/.build/server.key +++ b/.build/server.key @@ -1,27 +1,52 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAvC6EG6NgxcAte4GcCfMazyhxl1CsaKOVHMmd/9s2dNOk2JLM -LLHoypLLoiAkzCPPTg/J+eaTU2ZFz1B+6EdqCMsU2Eolj4jc2xIdBTsR/SoeYJmo -6jP+F+ODmqa5skOepuGNi2Ttp2fYR6HvmUaGp9/qWoPMHjjP5/8GLl2bXRJTsTbA -fKYNPSuIHrx1Wi2kgQhHkHx3r+dbwpRXTd/qKwYv64Z0Bd73cO6DE9SUh++UWsud -cEWsqSyLJQHNg3DxUwkCTJl1zE3AHMZowFVBq32PnbEgnV5zmbXe8pPtF9G1Riiw -ZAkzj+0KZnzlbvWzobHA4iuRKAGgyqnR0jFH4QIDAQABAoIBADnMS7U1dAao5Q9X -GrcPnP9dm63vEFU/URA7eLTZ/prZWntOczmTFz4I4lSUbNjqcsS2IsIHqN5nvi9T -uPbc4Ft9DJT2CR1R2wvKP3GY2AibBCOFbpUojPWHYqeAZ+6xyCvXgSL8R+YwBgTS -XwYD3F35b0CH1Iy/xFOsR5i8FXj7He8lOBA76fPrH64DEBTB2zUGztu4qpfv57v5 -sfTISi2ZOqPpXc+8Fw0RPeVWQgSRUh7U3lzL8bNBod6lYcjkhF5Yqet4MdHSyWMT -aKdZ2GRHHdWjpyx6J0cD/bjjaTSDqTD8r265mPzY6bq4t6UQMq4KeDnbeiextDf4 -ELT90YUCgYEA6insCSDJddhFZ51guPPyYE9GL8QQfnzLvFOA4qWsi0u9SAbJ9aS0 -vABaEuot0PyYPwMYq7st07z3DSKno4tisPJ2X7v2nEWxv8MjgczWpltPTPaEdmZE -WGIwG3pyh5wJk1b3VpBJB5jkjtJfGmUJaezU10bzm4QhPiEawemCjucCgYEAzbri -/6EZPbJJa9hGtkJEEVLwbQ2U/CE7mZXL+AcPlS3qMSwyz/1OArPxdTRR4S3sYRRO -fsRDBL8LED/kKUDWNni/zkzmFf/hVkmGd9zc6eif4Zr1gmtHlsHQdaMGxsomzxGL -qydBqDN+4TMmHmUmp2jR/0LIF5UMlNoCvHcxgfcCgYEAnOBNE6h1j4++n7Yd0IsO -PFufx+xwqGzvCVJgLHeV6xRo0NJLh1g7BSCvN7DP1Q0E6mImqxaRkyMr2A75hGWj -TqyBhY2ln/hJJxGSvij/PSA7NnKJN9E3xIazeBVGmXd+Ksm+lq2/X2mc5domgMZj -0iUqSrdsCSoyIy+Gf5bzMs0CgYBcquG044vLDpOj0DeJwS+H3iQN+yAwsYd3FtJZ -VlTejV//5ji9Fwwci5EnifmXxGfFErCIyT6m1KbXGvBa5KmYv6sl8d1x62BEzbmU -JBgeBHp/1JzhshD9BzAuzNAwmr4AZ5bR8UzRxuBP8AorhsRyg/STVjFq7ehM5CZ3 -Xfke4QKBgHCPo3R/oi/E2E7OIM/ELlDpvPQTMrV+rYlMFsy3JRvataIqEGnVbhOR -4dQHEM3u2bJxN79wUYYmZuymVB78wKxTn6hGWcGoM6Y8mrJjVv9D8V0Gc0sWw5pF -KZxuCgzjaN2T7i1LsXEV3gaQrKItToEpGPzSI23egFaG6g5SFqBt ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDlGT9vXb93yoM1 +YT0GAxJIB6/2ExUrdprd049oMVZa4Km0nqwN/xjVvQRIWozmbpvps0mCkFM1ZyL1 +iqZFwiJGWcQvvIffFM1qKRMOSTLNPCbM9mfvRKsCU9gjgatdhy8xUZhz7uFGMGAD +nZdlNMYWGgzMVZo0EyW7Z2QJ+ZCl8wW5IT4iswZWrJsNZU/g7HaNBrXiidDihkmQ +8Kt32R0UnqJeXMHwkQLxddmcGdDmVCKsAEUu3NcvPeAlSJsNHfGDRsf9fImRqZCs +gwI8dJtAke/luMTttQ34aADFTmTbVk4ngVhCxgBkJ6FUDFJcp3t3nFssiisNon9k +5FwtJ3hle/QGM9IRdBvGVcOnZZuXXK2lLtakj5UWUik2xWA0hjX+DsFo7TPwKgZy +4zmWCRobW1e1NX52bqYFWZUKYLqbizllOd98o3yed58PhbF1/IuVEuOoiKu7rNdN +gzr8vgRPpWHQNXp3maCcZq2kWybADU2LQNUKAZLSw3nClcX8QVRAfvf8IyDZ/280 +EYRGu99VqLqDPLa1+3CNAb93J1ONvVjKgJwQQWy4dYFLHTYdBzXV5SOpH8YHL/1I +Hs9W5k28BdwbeMtJnOaV8rqiA6Xd4Xem111AMAigHExxG3kpSnAq6jiOX0+2V++f +7qAunuC6B/oJATXLCbBQILr0ARtKuQIDAQABAoICAAP97y6VPnPLjgLVJxKbfssa +afz0IxG+9ZH11xrpUl6itjpNBUte8LN97jaF8DLhf9FJtZ2mWHJtODBfzw4wnldf +X/O2Y1MZbvHeXA3LHznXX9ROJ9krg/2DCsu/MIZgh5hvQLEmdK6Iw1q7LH5Pz6YA +Pea/YbPUfWGsVC0rUaBFB/C/oEnk/v0g8VIbFZIvAWrRw6oT0JWESJrGr5b9RYxm +Ljo0Mt0dyorjP/YAUI6u4R+VOp9g+Dvpv7909vfg/j2u5k20e/lgI1xdXqGnvrIx ++/4V/KwPeob9TIqJ/bTOGaFtF5j3dirImP8Yq6rsvSuqodkSSELeAor2XEsDumby +PqJY1MIO9DuZSdqf+Cofgzbd6mpeMAwueb+hfBw8AIMG3M9Xj1uDuU+tjsVA79Er +H9acPxLukGjYP5SY2Mo8hLFLLurpjtcDpYdOP2Wh7PBDwHR8anmPQru2rZXT80NY +j3fXNqnTTFbHuntmZ2qWJovmOuKocU5GEm/QCW/f6miqR9Hzc2vbWaIoEO54vcF6 +eS4iLEkAOfmakz3Sno2AXS1jJI6+2v1899cBINvgpATCMkmXnwFDwr9gNYujwlpF +Yl3QM8Vh9dnVt04oyum5x4sz/mTKj5e9O988iqlOkgID4HBVpy/dwYHsHE+XgDDY +yiFetJ/n0+45QHhyvSwBAoIBAQDnrPz2xCbR03KQwZN2DnZClLVFkZe3tZxR6UsY +63yDTrA0ZMJ8AtE/tX79/Iu7gPidNTCrVmOuelf5q3y3AMo6nlKMCc3tIKr6QtaC +99RtHq5p0T3/TS9tWbGjmxEzyx00R3wz5fSypX76qnQLHs6EmrLxFUNmsHIQS2nH +jWvT1+TdmfmogZ/9RaHyBjHGkDfTmlfEKc7/TleE9XsW+G0cGli3fIO0iY0hJTLd +b65X5Gm0URCqsZgIzD99enIvee13Gw8aUJUt8tJZXQHtOWBu491MLd2AVPQ/7eZa +tl/HtjdMj2E3n5NXTie3laRCX+p9mK6087nE7u3JqPqUXU4BAoIBAQD9Jv3hZeii +0pDgLYgiFVds5n2S4CEB4WOT9wn2vUIrYTSjgjAPfsgeJs6N6+WArwaIJrl4tTK4 +m0VjUG394plvyExU8hNZ7hw0E/33rwsKySnkwUFZtOgbsOgUjajRDfFYsqsDhLK0 +o3dY1M+mdYvU9OBo3EhgFy3fYBhtdGIq/4/3kSM6CARQIjddW2pdbB7pyv3qz0mH +6fpzPXWLIex+WBzRVEz7VPPD4coV3LEhmtdPju4RqFPbHS+OpECun8pyaNt14DRr +t216MiyJGNV74zTLELioVHlhlaPvsWnnIeI+2uhhCgQ8UvHn69x2wiAgLlx/e+RD +qPiINhm/xey5AoIBACCASjSsK+3/xfC8110Whkys5AlQdYJWPgnXuqtSTfN11I5l +HEudcZGIerpS9Z9mZnpXfe5rfix6CWGDR0m9GKHEmDwBHByKGrJlMgbJkcmFJl69 +9f6c62xhyuPy2yTy97Pf23LEbeGqCfhMdV8iAULlGPltTDlZw4a5ratLEbd0cC0O +btHO7YzwedmkONNsZAiRfIKOgvWaHfkPHyeHznbE03FaTHfFXEEsIMij5Ed8Sb/8 +J2Rq6bNCRB3sUZyLdF7jMuk0KNl7WTskKyMGi5rC6MbJIGvifymAzHIpZ6Jy06sv +6imNf3QeCMBeg96z6geYpdnI32TbSAykYhLyTAECggEAOowrCVcdX5LdaMt/AYr4 +BjqkbjShzaKH+i+XQVZyGEBKAUrZvKuwsrB88vvMv187Xn++Q3l8uo9Gk/qFBcPD +gsPLS5YU/aaBJVY+VWtJXXw60SoU6B9b0xOuCRreIUNdPwtLW+vzvK1Vq9jEEZZ7 ++YuM3xObNYYG2POLkrzo+1LRxArwH7q87J+NOG0tA2A/IgkNgqHgOqvVfZOIPN5i +qLHOMGeTykjSe8obh8Tbvo7mHwNKchEBG9r7Jb09LGXOV3mC0BdDaGoqyqkR/b8d +mKJqklBStLOcwwHtwUDB4m/GuIy+U7sSUbVJNz8oZNruvSKbx+wqVa+dkzsX529q +GQKCAQBVzafsrfp3yZKa62R7EMtQh6pHDIKvUzZRwxsj4QzQ1y4Rrb6ceXKxI3EQ +ZK6f1Lte+/ifRn8ZsxQOnjNzO9meOco/7CSNGCCcqO/XVN9ixDdF8lzjIsuRqfkT +lsYy7Zo+ZRDUj73UROBvBJtX4jP5It1B/ISKxHxyBFQiB+UtldLl1H+dmGN9LVnF +583i/vTEcLsj9+8yUU8L46sLKfOhNiSBY8D8oKD9Yht0p9SeDxB/r4Rq8Te5Xp1o +FobswNohYBj2rj9+d24uMcpI5nx33JoRkW7VyAXsq8t4b7ei5/sbwuL25NUXhIxf +mMKDxHebdrFY2ADhWLkWus0ik7JA +-----END PRIVATE KEY----- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20c6b93478..add3743612 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,9 +92,9 @@ jobs: sudo apt-get install -qq postgresql-${{ matrix.pg_major }} export PGDATA=/etc/postgresql/${{ matrix.pg_major }}/main - sudo cp $GITHUB_WORKSPACE/.build/{server.crt,server.key} $PGDATA - sudo chmod 600 $PGDATA/{server.crt,server.key} - sudo chown postgres $PGDATA/{server.crt,server.key} + sudo cp $GITHUB_WORKSPACE/.build/{server.crt,server.key,ca.crt} $PGDATA + sudo chmod 600 $PGDATA/{server.crt,server.key,ca.crt} + sudo chown postgres $PGDATA/{server.crt,server.key,ca.crt} # Create npgsql_tests user with md5 password 'npgsql_tests' sudo -u postgres psql -c "CREATE USER npgsql_tests SUPERUSER PASSWORD 'md5adf74603a5772843f53e812f03dacb02'" @@ -113,6 +113,7 @@ jobs: sudo sed -i 's/max_connections = 100/max_connections = 500/' $PGDATA/postgresql.conf sudo sed -i 's/#ssl = off/ssl = on/' $PGDATA/postgresql.conf + sudo sed -i "s|ssl_ca_file =|ssl_ca_file = '$PGDATA/ca.crt' #|" $PGDATA/postgresql.conf sudo sed -i "s|ssl_cert_file =|ssl_cert_file = '$PGDATA/server.crt' #|" $PGDATA/postgresql.conf sudo sed -i "s|ssl_key_file =|ssl_key_file = '$PGDATA/server.key' #|" $PGDATA/postgresql.conf sudo sed -i 's/#password_encryption = md5/password_encryption = scram-sha-256/' $PGDATA/postgresql.conf @@ -163,7 +164,7 @@ jobs: unzip pgsql.zip -x 'pgsql/include/**' 'pgsql/doc/**' 'pgsql/pgAdmin 4/**' 'pgsql/StackBuilder/**' # Match Npgsql CI Docker image and stash one level up - cp $GITHUB_WORKSPACE/.build/{server.crt,server.key} pgsql + cp $GITHUB_WORKSPACE/.build/{server.crt,server.key,ca.crt} pgsql # Find OSGEO version number OSGEO_VERSION=$(\ @@ -199,7 +200,7 @@ jobs: sed -i "s|#synchronous_standby_names =|synchronous_standby_names = 'npgsql_test_sync_standby' #|" pgsql/PGDATA/postgresql.conf sed -i "s|#synchronous_commit =|synchronous_commit = local #|" pgsql/PGDATA/postgresql.conf sed -i "s|#max_prepared_transactions = 0|max_prepared_transactions = 100|" pgsql/PGDATA/postgresql.conf - pgsql/bin/pg_ctl -D pgsql/PGDATA -l logfile -o '-c ssl=true -c ssl_cert_file=../server.crt -c ssl_key_file=../server.key' start + pgsql/bin/pg_ctl -D pgsql/PGDATA -l logfile -o '-c ssl=true -c ssl_cert_file=../server.crt -c ssl_key_file=../server.key -c ssl_ca_file=../ca.crt' start # Create npgsql_tests user with md5 password 'npgsql_tests' pgsql/bin/psql -U postgres -c "CREATE ROLE npgsql_tests SUPERUSER LOGIN PASSWORD 'md5adf74603a5772843f53e812f03dacb02'" @@ -214,7 +215,7 @@ jobs: sed -i "s|#password_encryption = md5|password_encryption = scram-sha-256|" pgsql/PGDATA/postgresql.conf fi - pgsql/bin/pg_ctl -D pgsql/PGDATA -l logfile -o '-c ssl=true -c ssl_cert_file=../server.crt -c ssl_key_file=../server.key' restart + pgsql/bin/pg_ctl -D pgsql/PGDATA -l logfile -o '-c ssl=true -c ssl_cert_file=../server.crt -c ssl_key_file=../server.key -c ssl_ca_file=../ca.crt' restart pgsql/bin/psql -U postgres -c "CREATE ROLE npgsql_tests_scram SUPERUSER LOGIN PASSWORD 'npgsql_tests_scram'" @@ -241,13 +242,14 @@ jobs: PGDATA=/opt/homebrew/var/postgresql@${{ matrix.pg_major }} sudo sed -i '' 's/#ssl = off/ssl = on/' $PGDATA/postgresql.conf - cp $GITHUB_WORKSPACE/.build/{server.crt,server.key} $PGDATA - chmod 600 $PGDATA/{server.crt,server.key} + sudo sed -i '' "s/#ssl_ca_file =/ssl_ca_file = 'ca.crt' #/" $PGDATA/postgresql.conf + cp $GITHUB_WORKSPACE/.build/{server.crt,server.key,ca.crt} $PGDATA + chmod 600 $PGDATA/{server.crt,server.key,ca.crt} postgreService=$(brew services list | grep -oe "postgresql@${{ matrix.pg_major }}\S*") brew services start $postgreService - export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" + export PATH="/opt/homebrew/opt/postgresql@${{ matrix.pg_major }}/bin:$PATH" echo "Check PostgreSQL service is running" i=5 COMMAND='pg_isready' diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 0d6c5e23d8..8d131912bd 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -917,7 +917,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, TargetHost = host, ClientCertificates = clientCertificates, EnabledSslProtocols = SslProtocols.None, - CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.Offline, + CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, RemoteCertificateValidationCallback = certificateValidationCallback, ApplicationProtocols = [_alpnProtocol] }; diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 6b7baca8ad..5d100b68b5 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -10,6 +10,11 @@ + + + PreserveNewest + + true $(NoWarn);NPG9001 diff --git a/test/Npgsql.Tests/SecurityTests.cs b/test/Npgsql.Tests/SecurityTests.cs index c1af68f515..f6451a633f 100644 --- a/test/Npgsql.Tests/SecurityTests.cs +++ b/test/Npgsql.Tests/SecurityTests.cs @@ -522,6 +522,47 @@ public void Direct_ssl_requires_correct_sslmode([Values] SslMode sslMode) } } + [Test] + [Platform(Exclude = "MacOsX", Reason = "Mac requires explicit opt-in to receive CA certificate in TLS handshake")] + public async Task Connect_with_verify_and_ca_cert([Values(SslMode.VerifyCA, SslMode.VerifyFull)] SslMode sslMode) + { + if (!IsOnBuildServer) + Assert.Ignore("Only executed in CI"); + + await using var dataSource = CreateDataSource(csb => + { + csb.SslMode = sslMode; + csb.RootCertificate = "ca.crt"; + }); + + await using var _ = await dataSource.OpenConnectionAsync(); + } + + [Test] + [Platform(Exclude = "MacOsX", Reason = "Mac requires explicit opt-in to receive CA certificate in TLS handshake")] + public async Task Connect_with_verify_check_host([Values(SslMode.VerifyCA, SslMode.VerifyFull)] SslMode sslMode) + { + if (!IsOnBuildServer) + Assert.Ignore("Only executed in CI"); + + await using var dataSource = CreateDataSource(csb => + { + csb.Host = "127.0.0.1"; + csb.SslMode = sslMode; + csb.RootCertificate = "ca.crt"; + }); + + if (sslMode == SslMode.VerifyCA) + { + await using var _ = await dataSource.OpenConnectionAsync(); + } + else + { + var ex = Assert.ThrowsAsync(async () => await dataSource.OpenConnectionAsync())!; + Assert.That(ex.InnerException, Is.TypeOf()); + } + } + [Test] [NonParallelizable] // Sets environment variable public async Task Direct_ssl_via_env_requires_correct_sslmode() From 62dcf217551b263a8fa21bb1336dc3cd57114360 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 17 Dec 2024 15:06:59 +0300 Subject: [PATCH 008/155] Remove stopwatch allocations (#5977) --- src/Npgsql/Internal/NpgsqlConnector.cs | 8 ++++---- src/Npgsql/MultiplexingDataSource.cs | 11 +++++------ src/Npgsql/NpgsqlEventSource.cs | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 8d131912bd..20586ce685 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -486,7 +486,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca State = ConnectorState.Connecting; LogMessages.OpeningPhysicalConnection(ConnectionLogger, Host, Port, Database, UserFacingConnectionString); - var stopwatch = Stopwatch.StartNew(); + var startOpenTimestamp = Stopwatch.GetTimestamp(); try { @@ -557,7 +557,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca } LogMessages.OpenedPhysicalConnection( - ConnectionLogger, Host, Port, Database, UserFacingConnectionString, stopwatch.ElapsedMilliseconds, Id); + ConnectionLogger, Host, Port, Database, UserFacingConnectionString, (long)Stopwatch.GetElapsedTime(startOpenTimestamp).TotalMilliseconds, Id); } catch (Exception e) { @@ -2646,7 +2646,7 @@ internal async Task Wait(bool async, int timeout, CancellationToken cancel LogMessages.SendingKeepalive(ConnectionLogger, Id); - var keepaliveTime = Stopwatch.StartNew(); + var keepaliveStartTimestamp = Stopwatch.GetTimestamp(); await WriteSync(async, cancellationToken).ConfigureAwait(false); await Flush(async, cancellationToken).ConfigureAwait(false); @@ -2687,7 +2687,7 @@ internal async Task Wait(bool async, int timeout, CancellationToken cancel } if (timeout > 0) - timeout -= (keepaliveMs + (int)keepaliveTime.ElapsedMilliseconds); + timeout -= (keepaliveMs + (int)Stopwatch.GetElapsedTime(keepaliveStartTimestamp).TotalMilliseconds); } } diff --git a/src/Npgsql/MultiplexingDataSource.cs b/src/Npgsql/MultiplexingDataSource.cs index 1d228e1f4c..74c32b8c6f 100644 --- a/src/Npgsql/MultiplexingDataSource.cs +++ b/src/Npgsql/MultiplexingDataSource.cs @@ -75,7 +75,7 @@ async Task MultiplexingWriteLoop() // on to the next connector. Debug.Assert(_multiplexCommandReader != null); - var stats = new MultiplexingStats { Stopwatch = new Stopwatch() }; + var stats = new MultiplexingStats(); while (true) { @@ -358,7 +358,7 @@ static void CompleteWrite(NpgsqlConnector connector, ref MultiplexingStats stats // for over-capacity write. connector.FlagAsWritableForMultiplexing(); - NpgsqlEventSource.Log.MultiplexingBatchSent(stats.NumCommands, stats.Stopwatch); + NpgsqlEventSource.Log.MultiplexingBatchSent(stats.NumCommands, Stopwatch.GetElapsedTime(stats.StartTimestamp).Ticks); } // ReSharper disable once FunctionNeverReturns @@ -380,19 +380,18 @@ protected override async ValueTask DisposeAsyncBase() struct MultiplexingStats { - internal Stopwatch Stopwatch; + internal long StartTimestamp; internal int NumCommands; internal void Reset() { NumCommands = 0; - Stopwatch.Reset(); + StartTimestamp = Stopwatch.GetTimestamp(); } internal MultiplexingStats Clone() { - var clone = new MultiplexingStats { Stopwatch = Stopwatch, NumCommands = NumCommands }; - Stopwatch = new Stopwatch(); + var clone = new MultiplexingStats { StartTimestamp = StartTimestamp, NumCommands = NumCommands }; return clone; } } diff --git a/src/Npgsql/NpgsqlEventSource.cs b/src/Npgsql/NpgsqlEventSource.cs index d50979bb64..82475142d2 100644 --- a/src/Npgsql/NpgsqlEventSource.cs +++ b/src/Npgsql/NpgsqlEventSource.cs @@ -101,14 +101,14 @@ internal void DataSourceCreated(NpgsqlDataSource dataSource) } } - internal void MultiplexingBatchSent(int numCommands, Stopwatch stopwatch) + internal void MultiplexingBatchSent(int numCommands, long elapsedTicks) { // TODO: CAS loop instead of 3 separate interlocked operations? if (IsEnabled()) { Interlocked.Increment(ref _multiplexingBatchesSent); Interlocked.Add(ref _multiplexingCommandsSent, numCommands); - Interlocked.Add(ref _multiplexingTicksWritten, stopwatch.ElapsedTicks); + Interlocked.Add(ref _multiplexingTicksWritten, elapsedTicks); } } From 523033601b8fd6e9b8642938929e025624884349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Dec 2024 05:10:53 +0200 Subject: [PATCH 009/155] Bump actions/setup-dotnet from 4.1.0 to 4.2.0 (#5983) --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/native-aot.yml | 4 ++-- .github/workflows/rich-code-nav.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index add3743612..ab854702fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} @@ -354,7 +354,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -388,7 +388,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 013421b14d..dbb8f48a39 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -66,7 +66,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 2f1b94a0d5..b599342c0e 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -108,7 +108,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} @@ -145,7 +145,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index 278b05c95a..9175897230 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -24,7 +24,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.1.0 + uses: actions/setup-dotnet@v4.2.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} From 010dc51ae61f9ca71d30ced4d025e87db4b7c17b Mon Sep 17 00:00:00 2001 From: Bruce Bowyer-Smyth Date: Sun, 29 Dec 2024 00:53:33 +1000 Subject: [PATCH 010/155] Use exception convenience methods (#5982) --- src/Npgsql/Internal/NpgsqlDatabaseInfo.cs | 3 +- .../Internal/NpgsqlReadBuffer.Stream.cs | 22 ++++------ src/Npgsql/Internal/NpgsqlReadBuffer.cs | 5 +-- src/Npgsql/Internal/NpgsqlWriteBuffer.cs | 6 +-- src/Npgsql/Internal/PgWriter.cs | 11 ++--- .../NpgsqlSnakeCaseNameTranslator.cs | 3 +- src/Npgsql/NpgsqlCommand.cs | 7 +--- src/Npgsql/NpgsqlConnection.cs | 23 ++++------- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 40 +++++++------------ src/Npgsql/NpgsqlDataSource.cs | 5 +-- src/Npgsql/NpgsqlLargeObjectStream.cs | 25 +++++------- src/Npgsql/NpgsqlNestedDataReader.cs | 7 ++-- src/Npgsql/NpgsqlParameter.cs | 8 ++-- src/Npgsql/NpgsqlParameterCollection.cs | 39 +++++++----------- src/Npgsql/NpgsqlRawCopyStream.cs | 11 ++--- src/Npgsql/NpgsqlSchema.cs | 3 +- src/Npgsql/NpgsqlTransaction.cs | 15 ++----- src/Npgsql/NpgsqlTypes/NpgsqlRange.cs | 3 +- src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs | 9 ++--- src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs | 3 +- src/Npgsql/PreparedTextReader.cs | 18 +++------ .../LogicalReplicationConnectionExtensions.cs | 6 +-- .../Replication/ReplicationConnection.cs | 6 +-- src/Npgsql/ThrowHelper.cs | 4 -- src/Npgsql/Util/SubReadStream.cs | 5 +-- src/Npgsql/VolatileResourceManager.cs | 5 +-- test/Npgsql.Tests/ConnectionTests.cs | 2 +- test/Npgsql.Tests/MultipleHostsTests.cs | 2 +- 28 files changed, 96 insertions(+), 200 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs index 7e3aebe237..aca4144a09 100644 --- a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs +++ b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs @@ -313,8 +313,7 @@ protected static Version ParseServerVersion(string value) /// public static void RegisterFactory(INpgsqlDatabaseInfoFactory factory) { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); + ArgumentNullException.ThrowIfNull(factory); var factories = new INpgsqlDatabaseInfoFactory[Factories.Length + 1]; factories[0] = factory; diff --git a/src/Npgsql/Internal/NpgsqlReadBuffer.Stream.cs b/src/Npgsql/Internal/NpgsqlReadBuffer.Stream.cs index 95b6c712f8..66f53503ed 100644 --- a/src/Npgsql/Internal/NpgsqlReadBuffer.Stream.cs +++ b/src/Npgsql/Internal/NpgsqlReadBuffer.Stream.cs @@ -73,8 +73,7 @@ public override long Position } set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), "Non - negative number required."); + ArgumentOutOfRangeException.ThrowIfNegative(value); Seek(value, SeekOrigin.Begin); } } @@ -85,8 +84,7 @@ public override long Seek(long offset, SeekOrigin origin) if (!_canSeek) throw new NotSupportedException(); - if (offset > int.MaxValue) - throw new ArgumentOutOfRangeException(nameof(offset), "Stream length must be non-negative and less than 2^31 - 1 - origin."); + ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, int.MaxValue); const string seekBeforeBegin = "An attempt was made to move the position before the beginning of the stream."; @@ -191,10 +189,7 @@ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); void CheckDisposed() - { - if (IsDisposed) - ThrowHelper.ThrowObjectDisposedException(nameof(ColumnStream)); - } + => ObjectDisposedException.ThrowIf(IsDisposed, this); protected override void Dispose(bool disposing) { @@ -224,13 +219,10 @@ async ValueTask DisposeCore(bool async) static void ValidateArguments(byte[] buffer, int offset, int count) { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - offset < count) - throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + ThrowHelper.ThrowArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); } } diff --git a/src/Npgsql/Internal/NpgsqlReadBuffer.cs b/src/Npgsql/Internal/NpgsqlReadBuffer.cs index a9b094efc2..4befd85146 100644 --- a/src/Npgsql/Internal/NpgsqlReadBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlReadBuffer.cs @@ -113,10 +113,7 @@ internal NpgsqlReadBuffer( Encoding relaxedTextEncoding, bool usePool = false) { - if (size < MinimumSize) - { - throw new ArgumentOutOfRangeException(nameof(size), size, "Buffer size must be at least " + MinimumSize); - } + ArgumentOutOfRangeException.ThrowIfLessThan(size, MinimumSize); Connector = connector!; // TODO: Clean this up Underlying = stream; diff --git a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs index 821bb7e6b1..c768020718 100644 --- a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs @@ -101,8 +101,7 @@ internal NpgsqlWriteBuffer( int size, Encoding textEncoding) { - if (size < MinimumSize) - throw new ArgumentOutOfRangeException(nameof(size), size, "Buffer size must be at least " + MinimumSize); + ArgumentOutOfRangeException.ThrowIfLessThan(size, MinimumSize); Connector = connector!; // TODO: Clean this up; only null when used from PregeneratedMessages, where we don't care. Underlying = stream; @@ -579,8 +578,7 @@ void AdvanceMessageBytesFlushed(int count) void Throw() { - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count), "Can't advance by a negative count"); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (_messageLength is null) throw Connector.Break(new InvalidOperationException("No message was started")); diff --git a/src/Npgsql/Internal/PgWriter.cs b/src/Npgsql/Internal/PgWriter.cs index 8dd0a9ba9f..fecf4b7474 100644 --- a/src/Npgsql/Internal/PgWriter.cs +++ b/src/Npgsql/Internal/PgWriter.cs @@ -495,14 +495,11 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati Task Write(bool async, byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - if (buffer is null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentNullException(nameof(offset)); - if (count < 0) - throw new ArgumentNullException(nameof(count)); + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - offset < count) - throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + ThrowHelper.ThrowArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); if (async) { diff --git a/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs b/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs index 760ddb1e5a..998b5f6420 100644 --- a/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs +++ b/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs @@ -55,8 +55,7 @@ public NpgsqlSnakeCaseNameTranslator(bool legacyMode, CultureInfo? culture = nul /// public string TranslateMemberName(string clrName) { - if (clrName == null) - throw new ArgumentNullException(nameof(clrName)); + ArgumentNullException.ThrowIfNull(clrName); return LegacyMode ? string.Concat(LegacyModeMap(clrName)).ToLower(_culture) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 012ce4cf56..f1ef8bb832 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -223,9 +223,7 @@ public override int CommandTimeout get => _timeout ?? (InternalConnection?.CommandTimeout ?? DefaultTimeout); set { - if (value < 0) { - throw new ArgumentOutOfRangeException(nameof(value), value, "CommandTimeout can't be less than zero."); - } + ArgumentOutOfRangeException.ThrowIfNegative(value); _timeout = value; } @@ -1955,8 +1953,7 @@ public virtual NpgsqlCommand Clone() NpgsqlConnection? CheckAndGetConnection() { - if (State is CommandState.Disposed) - ThrowHelper.ThrowObjectDisposedException(GetType().FullName); + ObjectDisposedException.ThrowIf(State is CommandState.Disposed, this); var conn = InternalConnection; if (conn is null) diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index 74290d23f9..4b989349f7 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -1145,8 +1145,7 @@ public Task BeginBinaryImportAsync(string copyFromCommand, async Task BeginBinaryImport(bool async, string copyFromCommand, CancellationToken cancellationToken = default) { - if (copyFromCommand == null) - throw new ArgumentNullException(nameof(copyFromCommand)); + ArgumentNullException.ThrowIfNull(copyFromCommand); if (!IsValidCopyCommand(copyFromCommand)) throw new ArgumentException("Must contain a COPY FROM STDIN command!", nameof(copyFromCommand)); @@ -1196,8 +1195,7 @@ public Task BeginBinaryExportAsync(string copyToCommand, C async Task BeginBinaryExport(bool async, string copyToCommand, CancellationToken cancellationToken = default) { - if (copyToCommand == null) - throw new ArgumentNullException(nameof(copyToCommand)); + ArgumentNullException.ThrowIfNull(copyToCommand); if (!IsValidCopyCommand(copyToCommand)) throw new ArgumentException("Must contain a COPY TO STDOUT command!", nameof(copyToCommand)); @@ -1253,8 +1251,7 @@ public Task BeginTextImportAsync(string copyFromCommand, Cancellatio async Task BeginTextImport(bool async, string copyFromCommand, CancellationToken cancellationToken = default) { - if (copyFromCommand == null) - throw new ArgumentNullException(nameof(copyFromCommand)); + ArgumentNullException.ThrowIfNull(copyFromCommand); if (!IsValidCopyCommand(copyFromCommand)) throw new ArgumentException("Must contain a COPY FROM STDIN command!", nameof(copyFromCommand)); @@ -1311,8 +1308,7 @@ public Task BeginTextExportAsync(string copyToCommand, CancellationT async Task BeginTextExport(bool async, string copyToCommand, CancellationToken cancellationToken = default) { - if (copyToCommand == null) - throw new ArgumentNullException(nameof(copyToCommand)); + ArgumentNullException.ThrowIfNull(copyToCommand); if (!IsValidCopyCommand(copyToCommand)) throw new ArgumentException("Must contain a COPY TO STDOUT command!", nameof(copyToCommand)); @@ -1369,8 +1365,7 @@ public Task BeginRawBinaryCopyAsync(string copyCommand, Can async Task BeginRawBinaryCopy(bool async, string copyCommand, CancellationToken cancellationToken = default) { - if (copyCommand == null) - throw new ArgumentNullException(nameof(copyCommand)); + ArgumentNullException.ThrowIfNull(copyCommand); if (!IsValidCopyCommand(copyCommand)) throw new ArgumentException("Must contain a COPY TO STDOUT OR COPY FROM STDIN command!", nameof(copyCommand)); @@ -1534,10 +1529,7 @@ void CheckClosed() } void CheckDisposed() - { - if (_disposed) - ThrowHelper.ThrowObjectDisposedException(nameof(NpgsqlConnection)); - } + => ObjectDisposedException.ThrowIf(_disposed, this); internal void CheckReady() { @@ -1825,8 +1817,7 @@ public async ValueTask CloneWithAsync(string connectionString, /// The name of the database to use in place of the current database. public override void ChangeDatabase(string dbName) { - if (dbName == null) - throw new ArgumentNullException(nameof(dbName)); + ArgumentNullException.ThrowIfNull(dbName); if (string.IsNullOrEmpty(dbName)) throw new ArgumentOutOfRangeException(nameof(dbName), dbName, $"Invalid database name: {dbName}"); diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index f662dd4a83..ca0629d7f7 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -244,8 +244,7 @@ public int Port get => _port; set { - if (value <= 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "Invalid port: " + value); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); _port = value; SetValue(nameof(Port), value); @@ -720,8 +719,7 @@ public int MinPoolSize get => _minPoolSize; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "MinPoolSize can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _minPoolSize = value; SetValue(nameof(MinPoolSize), value); @@ -742,8 +740,7 @@ public int MaxPoolSize get => _maxPoolSize; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "MaxPoolSize can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _maxPoolSize = value; SetValue(nameof(MaxPoolSize), value); @@ -836,8 +833,8 @@ public int Timeout get => _timeout; set { - if (value < 0 || value > NpgsqlConnection.TimeoutLimit) - throw new ArgumentOutOfRangeException(nameof(value), value, "Timeout must be between 0 and " + NpgsqlConnection.TimeoutLimit); + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, NpgsqlConnection.TimeoutLimit); _timeout = value; SetValue(nameof(Timeout), value); @@ -861,8 +858,7 @@ public int CommandTimeout get => _commandTimeout; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "CommandTimeout can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _commandTimeout = value; SetValue(nameof(CommandTimeout), value); @@ -885,8 +881,7 @@ public int CancellationTimeout get => _cancellationTimeout; set { - if (value < -1) - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(CancellationTimeout)} can't less than -1"); + ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); _cancellationTimeout = value; SetValue(nameof(CancellationTimeout), value); @@ -975,8 +970,7 @@ public int HostRecheckSeconds get => _hostRecheckSeconds; set { - if (value < 0) - throw new ArgumentException($"{HostRecheckSeconds} cannot be negative", nameof(HostRecheckSeconds)); + ArgumentOutOfRangeException.ThrowIfNegative(value); _hostRecheckSeconds = value; SetValue(nameof(HostRecheckSeconds), value); } @@ -1000,8 +994,7 @@ public int KeepAlive get => _keepAlive; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "KeepAlive can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _keepAlive = value; SetValue(nameof(KeepAlive), value); @@ -1041,8 +1034,7 @@ public int TcpKeepAliveTime get => _tcpKeepAliveTime; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "TcpKeepAliveTime can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _tcpKeepAliveTime = value; SetValue(nameof(TcpKeepAliveTime), value); @@ -1063,8 +1055,7 @@ public int TcpKeepAliveInterval get => _tcpKeepAliveInterval; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, "TcpKeepAliveInterval can't be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _tcpKeepAliveInterval = value; SetValue(nameof(TcpKeepAliveInterval), value); @@ -1160,8 +1151,7 @@ public int MaxAutoPrepare get => _maxAutoPrepare; set { - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(MaxAutoPrepare)} cannot be negative"); + ArgumentOutOfRangeException.ThrowIfNegative(value); _maxAutoPrepare = value; SetValue(nameof(MaxAutoPrepare), value); @@ -1183,8 +1173,7 @@ public int AutoPrepareMinUsages get => _autoPrepareMinUsages; set { - if (value < 1) - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(AutoPrepareMinUsages)} must be 1 or greater"); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); _autoPrepareMinUsages = value; SetValue(nameof(AutoPrepareMinUsages), value); @@ -1408,8 +1397,7 @@ public int InternalCommandTimeout internal void PostProcessAndValidate() { - if (string.IsNullOrWhiteSpace(Host)) - throw new ArgumentException("Host can't be null"); + ArgumentException.ThrowIfNullOrWhiteSpace(Host); if (Multiplexing && !Pooling) throw new ArgumentException("Pooling must be on to use multiplexing"); if (SslNegotiation == SslNegotiation.Direct && SslMode is not SslMode.Require and not SslMode.VerifyCA and not SslMode.VerifyFull) diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index ed434ee3c1..7d78fca230 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -533,10 +533,7 @@ protected virtual async ValueTask DisposeAsyncBase() } private protected void CheckDisposed() - { - if (_isDisposed == 1) - ThrowHelper.ThrowObjectDisposedException(GetType().FullName); - } + => ObjectDisposedException.ThrowIf(_isDisposed == 1, this); #endregion diff --git a/src/Npgsql/NpgsqlLargeObjectStream.cs b/src/Npgsql/NpgsqlLargeObjectStream.cs index 2f3c8b19b0..09d90b164a 100644 --- a/src/Npgsql/NpgsqlLargeObjectStream.cs +++ b/src/Npgsql/NpgsqlLargeObjectStream.cs @@ -64,14 +64,11 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel async Task Read(bool async, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - offset < count) - throw new ArgumentException("Invalid offset or count for this buffer"); + ThrowHelper.ThrowArgumentException("Invalid offset or count for this buffer"); CheckDisposed(); @@ -115,14 +112,11 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati async Task Write(bool async, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - offset < count) - throw new ArgumentException("Invalid offset or count for this buffer"); + ThrowHelper.ThrowArgumentException("Invalid offset or count for this buffer"); CheckDisposed(); @@ -262,8 +256,7 @@ async Task SetLength(bool async, long value, CancellationToken cancellationToken { cancellationToken.ThrowIfCancellationRequested(); - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value)); + ArgumentOutOfRangeException.ThrowIfNegative(value); if (!Has64BitSupport && value != (int)value) throw new ArgumentOutOfRangeException(nameof(value), "offset must fit in 32 bits for PostgreSQL versions older than 9.3"); diff --git a/src/Npgsql/NpgsqlNestedDataReader.cs b/src/Npgsql/NpgsqlNestedDataReader.cs index b505fe04f0..cda412d1a5 100644 --- a/src/Npgsql/NpgsqlNestedDataReader.cs +++ b/src/Npgsql/NpgsqlNestedDataReader.cs @@ -174,8 +174,8 @@ public override bool IsClosed /// public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) { - if (dataOffset is < 0 or > int.MaxValue) - throw new ArgumentOutOfRangeException(nameof(dataOffset), dataOffset, $"dataOffset must be between 0 and {int.MaxValue}"); + ArgumentOutOfRangeException.ThrowIfNegative(dataOffset); + ArgumentOutOfRangeException.ThrowIfGreaterThan(dataOffset, int.MaxValue); if (buffer != null && (bufferOffset < 0 || bufferOffset >= buffer.Length + 1)) throw new IndexOutOfRangeException($"bufferOffset must be between 0 and {buffer.Length}"); if (buffer != null && (length < 0 || length > buffer.Length - bufferOffset)) @@ -303,8 +303,7 @@ public override object GetValue(int ordinal) /// public override int GetValues(object[] values) { - if (values == null) - throw new ArgumentNullException(nameof(values)); + ArgumentNullException.ThrowIfNull(values); CheckOnRow(); var count = Math.Min(FieldCount, values.Length); diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index 6273a9617a..b1318d9b0a 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -365,9 +365,9 @@ public NpgsqlDbType NpgsqlDbType set { if (value == NpgsqlDbType.Array) - throw new ArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Array, Binary-Or with the element type (e.g. Array of Box is NpgsqlDbType.Array | NpgsqlDbType.Box)."); + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Array, Binary-Or with the element type (e.g. Array of Box is NpgsqlDbType.Array | NpgsqlDbType.Box)."); if (value == NpgsqlDbType.Range) - throw new ArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Range, Binary-Or with the element type (e.g. Range of integer is NpgsqlDbType.Range | NpgsqlDbType.Integer)"); + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Range, Binary-Or with the element type (e.g. Range of integer is NpgsqlDbType.Range | NpgsqlDbType.Integer)"); ResetTypeInfo(); _npgsqlDbType = value; @@ -453,7 +453,7 @@ public sealed override int Size set { if (value < -1) - throw new ArgumentException($"Invalid parameter Size value '{value}'. The value must be greater than or equal to 0."); + ThrowHelper.ThrowArgumentException($"Invalid parameter Size value '{value}'. The value must be greater than or equal to 0."); ResetBindingInfo(); _size = value; @@ -599,7 +599,7 @@ void ThrowNoTypeInfo() void ThrowNotSupported(string dataTypeName) { - throw new NotSupportedException(_npgsqlDbType is not null + ThrowHelper.ThrowNotSupportedException(_npgsqlDbType is not null ? $"The NpgsqlDbType '{_npgsqlDbType}' isn't present in your database. You may need to install an extension or upgrade to a newer version." : $"The data type name '{dataTypeName}' isn't present in your database. You may need to install an extension or upgrade to a newer version."); } diff --git a/src/Npgsql/NpgsqlParameterCollection.cs b/src/Npgsql/NpgsqlParameterCollection.cs index 8031fd7efc..b2c56d7ac7 100644 --- a/src/Npgsql/NpgsqlParameterCollection.cs +++ b/src/Npgsql/NpgsqlParameterCollection.cs @@ -166,28 +166,25 @@ internal void ChangeParameterName(NpgsqlParameter parameter, string? value) { get { - if (parameterName is null) - throw new ArgumentNullException(nameof(parameterName)); + ArgumentNullException.ThrowIfNull(parameterName); var index = IndexOf(parameterName); if (index == -1) - throw new ArgumentException("Parameter not found"); + ThrowHelper.ThrowArgumentException("Parameter not found"); return InternalList[index]; } set { - if (parameterName is null) - throw new ArgumentNullException(nameof(parameterName)); - if (value is null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(parameterName); + ArgumentNullException.ThrowIfNull(value); var index = IndexOf(parameterName); if (index == -1) - throw new ArgumentException("Parameter not found"); + ThrowHelper.ThrowArgumentException("Parameter not found"); if (!string.Equals(parameterName, value.TrimmedName, StringComparison.OrdinalIgnoreCase)) - throw new ArgumentException("Parameter name must be a case-insensitive match with the property 'ParameterName' on the given NpgsqlParameter", nameof(parameterName)); + ThrowHelper.ThrowArgumentException("Parameter name must be a case-insensitive match with the property 'ParameterName' on the given NpgsqlParameter", nameof(parameterName)); var oldValue = InternalList[index]; LookupChangeName(value, oldValue.ParameterName, oldValue.TrimmedName, index); @@ -206,8 +203,7 @@ internal void ChangeParameterName(NpgsqlParameter parameter, string? value) get => InternalList[index]; set { - if (value is null) - ThrowHelper.ThrowArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); if (value.Collection is not null) ThrowHelper.ThrowInvalidOperationException("The parameter already belongs to a collection"); @@ -231,8 +227,7 @@ internal void ChangeParameterName(NpgsqlParameter parameter, string? value) /// The parameter that was added. public NpgsqlParameter Add(NpgsqlParameter value) { - if (value is null) - ThrowHelper.ThrowArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); if (value.Collection is not null) ThrowHelper.ThrowInvalidOperationException("The parameter already belongs to a collection"); @@ -430,8 +425,7 @@ void BuildLookup() /// The zero-based index of the parameter. public override void RemoveAt(int index) { - if (InternalList.Count - 1 < index) - throw new ArgumentOutOfRangeException(nameof(index)); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, InternalList.Count); Remove(InternalList[index]); } @@ -446,8 +440,7 @@ public override void Insert(int index, object value) /// The name of the to remove from the collection. public void Remove(string parameterName) { - if (parameterName is null) - ThrowHelper.ThrowArgumentNullException(nameof(parameterName)); + ArgumentNullException.ThrowIfNull(parameterName); var index = IndexOf(parameterName); if (index < 0) @@ -481,8 +474,7 @@ public override bool Contains(object value) /// public bool TryGetValue(string parameterName, [NotNullWhen(true)] out NpgsqlParameter? parameter) { - if (parameterName is null) - throw new ArgumentNullException(nameof(parameterName)); + ArgumentNullException.ThrowIfNull(parameterName); var index = IndexOf(parameterName); @@ -561,8 +553,7 @@ IEnumerator IEnumerable.GetEnumerator() /// public override void AddRange(Array values) { - if (values is null) - throw new ArgumentNullException(nameof(values)); + ArgumentNullException.ThrowIfNull(values); foreach (var parameter in values) Add(Cast(parameter)); @@ -599,8 +590,7 @@ public int IndexOf(NpgsqlParameter item) /// Parameter to insert. public void Insert(int index, NpgsqlParameter item) { - if (item is null) - throw new ArgumentNullException(nameof(item)); + ArgumentNullException.ThrowIfNull(item); if (item.Collection != null) throw new Exception("The parameter already belongs to a collection"); @@ -624,8 +614,7 @@ public void Insert(int index, NpgsqlParameter item) /// True if the parameter was found and removed, otherwise false. public bool Remove(NpgsqlParameter item) { - if (item == null) - ThrowHelper.ThrowArgumentNullException(nameof(item)); + ArgumentNullException.ThrowIfNull(item); if (item.Collection != this) ThrowHelper.ThrowInvalidOperationException("The item does not belong to this collection"); diff --git a/src/Npgsql/NpgsqlRawCopyStream.cs b/src/Npgsql/NpgsqlRawCopyStream.cs index d7b818679a..3648b24075 100644 --- a/src/Npgsql/NpgsqlRawCopyStream.cs +++ b/src/Npgsql/NpgsqlRawCopyStream.cs @@ -445,14 +445,11 @@ public override long Position #region Input validation static void ValidateArguments(byte[] buffer, int offset, int count) { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentNullException(nameof(offset)); - if (count < 0) - throw new ArgumentNullException(nameof(count)); + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - offset < count) - throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + ThrowHelper.ThrowArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); } #endregion } diff --git a/src/Npgsql/NpgsqlSchema.cs b/src/Npgsql/NpgsqlSchema.cs index 7ce5f3ec1d..409fe3b91e 100644 --- a/src/Npgsql/NpgsqlSchema.cs +++ b/src/Npgsql/NpgsqlSchema.cs @@ -19,8 +19,7 @@ static class NpgsqlSchema { public static Task GetSchema(bool async, NpgsqlConnection conn, string? collectionName, string?[]? restrictions, CancellationToken cancellationToken = default) { - if (collectionName is null) - throw new ArgumentNullException(nameof(collectionName)); + ArgumentNullException.ThrowIfNull(collectionName); if (collectionName.Length == 0) throw new ArgumentException("Collection name cannot be empty.", nameof(collectionName)); diff --git a/src/Npgsql/NpgsqlTransaction.cs b/src/Npgsql/NpgsqlTransaction.cs index 1beebd0924..f88efbefa9 100644 --- a/src/Npgsql/NpgsqlTransaction.cs +++ b/src/Npgsql/NpgsqlTransaction.cs @@ -192,10 +192,7 @@ public override Task RollbackAsync(CancellationToken cancellationToken = default /// public override void Save(string name) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("name can't be empty", nameof(name)); + ArgumentException.ThrowIfNullOrWhiteSpace(name); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) @@ -236,10 +233,7 @@ public override Task SaveAsync(string name, CancellationToken cancellationToken async Task Rollback(bool async, string name, CancellationToken cancellationToken = default) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("name can't be empty", nameof(name)); + ArgumentException.ThrowIfNullOrWhiteSpace(name); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) @@ -271,10 +265,7 @@ public override Task RollbackAsync(string name, CancellationToken cancellationTo async Task Release(bool async, string name, CancellationToken cancellationToken = default) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("name can't be empty", nameof(name)); + ArgumentException.ThrowIfNullOrWhiteSpace(name); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlRange.cs b/src/Npgsql/NpgsqlTypes/NpgsqlRange.cs index b447cb5df7..23b2578c13 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlRange.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlRange.cs @@ -378,8 +378,7 @@ public override string ToString() [RequiresUnreferencedCode("Parse implementations for certain types of T may require members that have been trimmed.")] public static NpgsqlRange Parse(string value) { - if (value is null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); value = value.Trim(); diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs index bb1629705c..96585832f3 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTsQuery.cs @@ -79,8 +79,7 @@ public override string ToString() [Obsolete("Client-side parsing of NpgsqlTsQuery is unreliable and cannot fully duplicate the PostgreSQL logic. Use PG functions instead (e.g. to_tsquery)")] public static NpgsqlTsQuery Parse(string value) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); var valStack = new Stack(); var opStack = new Stack(); @@ -404,8 +403,7 @@ public string Text get => _text; set { - if (string.IsNullOrEmpty(value)) - throw new ArgumentException("Text is null or empty string", nameof(value)); + ArgumentException.ThrowIfNullOrEmpty(value); _text = value; } @@ -675,8 +673,7 @@ public NpgsqlTsQueryFollowedBy( NpgsqlTsQuery right) : base(NodeKind.Phrase, left, right) { - if (distance < 0) - throw new ArgumentOutOfRangeException(nameof(distance)); + ArgumentOutOfRangeException.ThrowIfNegative(distance); Distance = distance; } diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs index 2cf1bcb3f7..7d63a547fe 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs @@ -76,8 +76,7 @@ internal NpgsqlTsVector(List lexemes, bool noCheck = false) [Obsolete("Client-side parsing of NpgsqlTsVector is unreliable and cannot fully duplicate the PostgreSQL logic. Use PG functions instead (e.g. to_tsvector)")] public static NpgsqlTsVector Parse(string value) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); var lexemes = new List(); var pos = 0; diff --git a/src/Npgsql/PreparedTextReader.cs b/src/Npgsql/PreparedTextReader.cs index 4831850684..80ee543d9b 100644 --- a/src/Npgsql/PreparedTextReader.cs +++ b/src/Npgsql/PreparedTextReader.cs @@ -57,17 +57,12 @@ public override int Read(Span buffer) public override int Read(char[] buffer, int index, int count) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (index < 0 || count < 0) - { - throw new ArgumentOutOfRangeException(index < 0 ? nameof(index) : nameof(count)); - } + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfNegative(count); if (buffer.Length - index < count) { - throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + ThrowHelper.ThrowArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); } return Read(buffer.AsSpan(index, count)); @@ -95,10 +90,7 @@ public override string ReadToEnd() public override Task ReadToEndAsync() => Task.FromResult(ReadToEnd()); void CheckDisposed() - { - if (_disposed || _stream.IsDisposed) - ThrowHelper.ThrowObjectDisposedException(nameof(PreparedTextReader)); - } + => ObjectDisposedException.ThrowIf(_disposed || _stream.IsDisposed, this); public void Restart() { diff --git a/src/Npgsql/Replication/Internal/LogicalReplicationConnectionExtensions.cs b/src/Npgsql/Replication/Internal/LogicalReplicationConnectionExtensions.cs index 6f703970de..d66a9e55d1 100644 --- a/src/Npgsql/Replication/Internal/LogicalReplicationConnectionExtensions.cs +++ b/src/Npgsql/Replication/Internal/LogicalReplicationConnectionExtensions.cs @@ -61,10 +61,8 @@ public static Task CreateLogicalReplicationSlot( CancellationToken cancellationToken = default) { connection.CheckDisposed(); - if (slotName is null) - throw new ArgumentNullException(nameof(slotName)); - if (outputPlugin is null) - throw new ArgumentNullException(nameof(outputPlugin)); + ArgumentNullException.ThrowIfNull(slotName); + ArgumentNullException.ThrowIfNull(outputPlugin); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Npgsql/Replication/ReplicationConnection.cs b/src/Npgsql/Replication/ReplicationConnection.cs index 575efb669b..94fe30ab25 100644 --- a/src/Npgsql/Replication/ReplicationConnection.cs +++ b/src/Npgsql/Replication/ReplicationConnection.cs @@ -322,8 +322,7 @@ public async Task IdentifySystem(CancellationTo /// The current setting of the run-time parameter specified in as . public Task Show(string parameterName, CancellationToken cancellationToken = default) { - if (parameterName is null) - throw new ArgumentNullException(nameof(parameterName)); + ArgumentNullException.ThrowIfNull(parameterName); return ShowInternal(parameterName, cancellationToken); @@ -710,8 +709,7 @@ async void TimerSendFeedback(object? obj) /// A task representing the asynchronous drop operation. public Task DropReplicationSlot(string slotName, bool wait = false, CancellationToken cancellationToken = default) { - if (slotName is null) - throw new ArgumentNullException(nameof(slotName)); + ArgumentNullException.ThrowIfNull(slotName); CheckDisposed(); diff --git a/src/Npgsql/ThrowHelper.cs b/src/Npgsql/ThrowHelper.cs index 1c754884ab..dc79128537 100644 --- a/src/Npgsql/ThrowHelper.cs +++ b/src/Npgsql/ThrowHelper.cs @@ -88,10 +88,6 @@ internal static void ThrowArgumentException(string message) internal static void ThrowArgumentException(string message, string paramName) => throw new ArgumentException(message, paramName); - [DoesNotReturn] - internal static void ThrowArgumentNullException(string paramName) - => throw new ArgumentNullException(paramName); - [DoesNotReturn] internal static void ThrowArgumentNullException(string message, string paramName) => throw new ArgumentNullException(paramName, message); diff --git a/src/Npgsql/Util/SubReadStream.cs b/src/Npgsql/Util/SubReadStream.cs index 9f0176b631..8d9d1b1ec5 100644 --- a/src/Npgsql/Util/SubReadStream.cs +++ b/src/Npgsql/Util/SubReadStream.cs @@ -75,10 +75,7 @@ public override long Position public override bool CanWrite => false; void ThrowIfDisposed() - { - if (_isDisposed) - throw new ObjectDisposedException(GetType().ToString()); - } + => ObjectDisposedException.ThrowIf(_isDisposed, this); void ThrowIfCantRead() { diff --git a/src/Npgsql/VolatileResourceManager.cs b/src/Npgsql/VolatileResourceManager.cs index 2e2d698834..92a716f2e2 100644 --- a/src/Npgsql/VolatileResourceManager.cs +++ b/src/Npgsql/VolatileResourceManager.cs @@ -293,10 +293,7 @@ void Dispose() #pragma warning restore CS8625 void CheckDisposed() - { - if (_isDisposed) - throw new ObjectDisposedException(nameof(VolatileResourceManager)); - } + => ObjectDisposedException.ThrowIf(_isDisposed, this); #endregion diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 151255b8bc..15a550fe50 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -217,7 +217,7 @@ public void Bad_database() [Test, Description("Tests that mandatory connection string parameters are indeed mandatory")] public void Mandatory_connection_string_params() - => Assert.Throws(() => + => Assert.Throws(() => new NpgsqlConnection("User ID=npgsql_tests;Password=npgsql_tests;Database=npgsql_tests")); [Test, Description("Reuses the same connection instance for a failed connection, then a successful one")] diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index 662c08d5b9..e09cbae401 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -323,7 +323,7 @@ public void HostRecheckSeconds_zero_value() [Test] public void HostRecheckSeconds_invalid_throws() - => Assert.Throws(() => + => Assert.Throws(() => new NpgsqlConnectionStringBuilder { HostRecheckSeconds = -1 From 8effce1e3fbc851a66c6ca65101a26961ed325be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:35:12 +0000 Subject: [PATCH 011/155] Bump actions/setup-dotnet from 4.2.0 to 4.3.0 (#6007) --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/native-aot.yml | 4 ++-- .github/workflows/rich-code-nav.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab854702fa..8547e3b8d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} @@ -354,7 +354,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -388,7 +388,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dbb8f48a39..edfd1fbd0f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -66,7 +66,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index b599342c0e..78c713c45b 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -108,7 +108,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} @@ -145,7 +145,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: | ${{ env.dotnet_sdk_version }} diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index 9175897230..e007cab721 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -24,7 +24,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.2.0 + uses: actions/setup-dotnet@v4.3.0 with: dotnet-version: ${{ env.dotnet_sdk_version }} From 3505dc3abd7d32b706c97b26fbf8502e511c70a7 Mon Sep 17 00:00:00 2001 From: dvas-hash Date: Tue, 4 Feb 2025 12:37:03 +0100 Subject: [PATCH 012/155] Add support for postgresql type names with dots (#5971) Fixes #5972 --------- Co-authored-by: Dmitry Vasliyev --- src/Npgsql/Internal/Postgres/DataTypeName.cs | 4 +++- test/Npgsql.Tests/Support/TestBase.cs | 12 ++++++++--- test/Npgsql.Tests/Types/CompositeTests.cs | 22 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Npgsql/Internal/Postgres/DataTypeName.cs b/src/Npgsql/Internal/Postgres/DataTypeName.cs index c5b223f866..616881f385 100644 --- a/src/Npgsql/Internal/Postgres/DataTypeName.cs +++ b/src/Npgsql/Internal/Postgres/DataTypeName.cs @@ -127,12 +127,14 @@ public static DataTypeName FromDisplayName(string displayName, string? schema = // There is one exception and that's array syntax, which is always resolvable in both ways, while we want the canonical name. var schemaEndIndex = displayNameSpan.IndexOf('.'); if (schemaEndIndex is not -1 && + string.IsNullOrEmpty(schema) && !displayNameSpan.Slice(schemaEndIndex).StartsWith("_".AsSpan(), StringComparison.Ordinal) && !displayNameSpan.EndsWith("[]".AsSpan(), StringComparison.Ordinal)) return new(displayName); // First we strip the schema to get the type name. - if (schemaEndIndex is not -1) + if (schemaEndIndex is not -1 && + string.IsNullOrEmpty(schema)) { schema = displayNameSpan.Slice(0, schemaEndIndex).ToString(); displayNameSpan = displayNameSpan.Slice(schemaEndIndex + 1); diff --git a/test/Npgsql.Tests/Support/TestBase.cs b/test/Npgsql.Tests/Support/TestBase.cs index 66cdfb6780..61c4e2accf 100644 --- a/test/Npgsql.Tests/Support/TestBase.cs +++ b/test/Npgsql.Tests/Support/TestBase.cs @@ -201,7 +201,10 @@ internal static async Task AssertTypeReadCore( if (dotIndex > -1 && dataTypeName.Substring(0, dotIndex) is "pg_catalog" or "public") dataTypeName = dataTypeName.Substring(dotIndex + 1); - Assert.That(dataTypeName, Is.EqualTo(pgTypeName), + // For composite type with dots, postgres works only with quoted name - scheme."My.type.name" + // but npgsql converts it to name without quotes + var pgTypeNameWithoutQuotes = dataTypeName.Replace("\"", string.Empty); + Assert.That(dataTypeName, Is.EqualTo(pgTypeNameWithoutQuotes), $"Got wrong result from GetDataTypeName when reading '{truncatedSqlLiteral}'"); if (isDefault) @@ -300,9 +303,12 @@ internal static async Task AssertTypeWriteCore( } // With data type name - p = new NpgsqlParameter { Value = valueFactory(), DataTypeName = pgTypeNameWithoutFacets }; + // For composite type with dots in name, Postgresql returns name with quotes - scheme."My.type.name" + // but for npgsql mapping we should use names without quotes - scheme.My.type.name + var pgTypeNameWithoutFacetsAndDots = pgTypeNameWithoutFacets.Replace("\"", string.Empty); + p = new NpgsqlParameter { Value = valueFactory(), DataTypeName = pgTypeNameWithoutFacetsAndDots }; cmd.Parameters.Add(p); - errorIdentifier[++errorIdentifierIndex] = $"DataTypeName={pgTypeNameWithoutFacets}"; + errorIdentifier[++errorIdentifierIndex] = $"DataTypeName={pgTypeNameWithoutFacetsAndDots}"; CheckInference(); // With DbType diff --git a/test/Npgsql.Tests/Types/CompositeTests.cs b/test/Npgsql.Tests/Types/CompositeTests.cs index baaed149f3..765508908c 100644 --- a/test/Npgsql.Tests/Types/CompositeTests.cs +++ b/test/Npgsql.Tests/Types/CompositeTests.cs @@ -202,6 +202,28 @@ await AssertType( isDefaultForWriting: true); } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/5972")] + public async Task With_schema_and_dots_in_type_name() + { + await using var adminConnection = await OpenConnectionAsync(); + var schema = await CreateTempSchema(adminConnection); + var typename = "Some.Composite.with.dots"; + + await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {schema}.\"{typename}\" AS (x int, some_text text)"); + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.MapComposite($"{schema}.{typename}"); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + await AssertType( + connection, + new SomeComposite { SomeText = "foobar", X = 10 }, + "(10,foobar)", + $"{schema}.\"{typename}\"", + npgsqlDbType: null); + } + [Test] public async Task Struct() { From e8664e596d9a16caafbabfbe58bdd5f8b6d53acb Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 4 Feb 2025 14:41:00 +0300 Subject: [PATCH 013/155] Send close_notify TLS alert on connection shutdown (#5995) Fixes #5994 --- src/Npgsql/Internal/NpgsqlConnector.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 20586ce685..12e113a7a1 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -2189,10 +2189,25 @@ void FullCleanup() /// Closes the socket and cleans up client-side resources associated with this connector. /// /// - /// This method doesn't actually perform any meaningful I/O, and therefore is sync-only. + /// This method doesn't actually perform any meaningful I/O (except sending TLS alert), and therefore is sync-only. /// void Cleanup() { + if (_stream is SslStream sslStream) + { + try + { + // Send close_notify TLS alert to correctly close connection on postgres's side + sslStream.ShutdownAsync().GetAwaiter().GetResult(); + // Theoretically we should do a 0 read here to receive server's close_notify alert + // But overall it doesn't look like it makes much of a difference + } + catch + { + // ignored + } + } + try { _stream?.Dispose(); From 7f1a59fa8dc1ccc34a70154f49a768e1abf826ba Mon Sep 17 00:00:00 2001 From: Bruce Bowyer-Smyth Date: Fri, 7 Feb 2025 00:39:04 +1000 Subject: [PATCH 014/155] Remove DisplayClass struct creation in PgReader (#6014) --- src/Npgsql/Internal/PgReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/PgReader.cs b/src/Npgsql/Internal/PgReader.cs index 7fbaa695cd..5da3ea7681 100644 --- a/src/Npgsql/Internal/PgReader.cs +++ b/src/Npgsql/Internal/PgReader.cs @@ -744,10 +744,10 @@ public bool ShouldBuffer(Size bufferRequirement) => ShouldBuffer(GetBufferRequirementByteCount(bufferRequirement)); public bool ShouldBuffer(int byteCount) { - return _buffer.ReadBytesLeft < byteCount && ShouldBufferSlow(); + return _buffer.ReadBytesLeft < byteCount && ShouldBufferSlow(byteCount); [MethodImpl(MethodImplOptions.NoInlining)] - bool ShouldBufferSlow() + bool ShouldBufferSlow(int byteCount) { if (byteCount > _buffer.Size) ThrowHelper.ThrowArgumentOutOfRangeException(nameof(byteCount), From 44a7ab1cc95c9a649283e85390896b838a0b3bed Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Fri, 21 Feb 2025 12:55:16 +0300 Subject: [PATCH 015/155] Always dispose RemoteCertificate on SslStream (#6022) Fixes #5993 --- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 2 ++ src/Npgsql/Internal/NpgsqlConnector.cs | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 8771ce8e33..1cd2b6e697 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -196,6 +196,8 @@ internal void AuthenticateSASLSha256Plus(ref string mechanism, ref string cbindF return; } + // While SslStream.RemoteCertificate is X509Certificate2, it actually returns X509Certificate2 + // But to be on the safe side we'll just create a new instance of it using var remoteCertificate = new X509Certificate2(sslStream.RemoteCertificate); // Checking for hashing algorithms HashAlgorithm? hashAlgorithm = null; diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 12e113a7a1..8f2df119b0 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -2193,7 +2193,8 @@ void FullCleanup() /// void Cleanup() { - if (_stream is SslStream sslStream) + var sslStream = _stream as SslStream; + if (sslStream is not null) { try { @@ -2208,6 +2209,19 @@ void Cleanup() } } + // After we access SslStream.RemoteCertificate (like for SASLSha256Plus) + // SslStream will no longer dispose it for us automatically + // Which is why we have to do it ourselves before disposing the stream + // As otherwise accessing RemoteCertificate will throw an exception + try + { + sslStream?.RemoteCertificate?.Dispose(); + } + catch + { + // ignored + } + try { _stream?.Dispose(); From a46eab961295bd52178e8129888f54f9c8988cbf Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 24 Feb 2025 13:32:43 +0300 Subject: [PATCH 016/155] Remove LongRunningConnection field from NpgsqlConnector (#6024) --- src/Npgsql/Internal/NpgsqlConnector.cs | 4 ---- src/Npgsql/Internal/NpgsqlReadBuffer.cs | 2 +- src/Npgsql/Replication/ReplicationConnection.cs | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 8f2df119b0..b1a92df4b6 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -182,9 +182,6 @@ internal string InferredUserName /// volatile Exception? _breakReason; - // Used by replication to change our cancellation behaviour on ColumnStreams. - internal bool LongRunningConnection { get; set; } - /// /// /// Used by the pool to indicate that I/O is currently in progress on this connector, so that another write @@ -2399,7 +2396,6 @@ internal async Task Reset(bool async) [MethodImpl(MethodImplOptions.AggressiveInlining)] void ResetReadBuffer() { - LongRunningConnection = false; if (_origReadBuffer != null) { Debug.Assert(_origReadBuffer.ReadBytesLeft == 0); diff --git a/src/Npgsql/Internal/NpgsqlReadBuffer.cs b/src/Npgsql/Internal/NpgsqlReadBuffer.cs index 4befd85146..d8622fc7a1 100644 --- a/src/Npgsql/Internal/NpgsqlReadBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlReadBuffer.cs @@ -678,7 +678,7 @@ public ColumnStream CreateStream(int len, bool canSeek, bool consumeOnDispose = { if (_lastStream is not { IsDisposed: true }) _lastStream = new ColumnStream(Connector); - _lastStream.Init(len, canSeek, !Connector.LongRunningConnection, consumeOnDispose); + _lastStream.Init(len, canSeek, Connector.Settings.ReplicationMode == ReplicationMode.Off, consumeOnDispose); return _lastStream; } diff --git a/src/Npgsql/Replication/ReplicationConnection.cs b/src/Npgsql/Replication/ReplicationConnection.cs index 94fe30ab25..4a41467164 100644 --- a/src/Npgsql/Replication/ReplicationConnection.cs +++ b/src/Npgsql/Replication/ReplicationConnection.cs @@ -237,8 +237,6 @@ public async Task Open(CancellationToken cancellationToken = default) SetTimeouts(CommandTimeout, CommandTimeout); - _npgsqlConnection.Connector!.LongRunningConnection = true; - ReplicationLogger = _npgsqlConnection.Connector!.LoggingConfiguration.ReplicationLogger; } From 01155b635f36976c14b4b53fff918d75c34f6928 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 24 Feb 2025 14:05:39 +0300 Subject: [PATCH 017/155] Tighten SCRAM-SHA-256 SASL check (#6023) --- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 10 +++++----- src/Npgsql/Internal/NpgsqlConnector.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 1cd2b6e697..7d53040bac 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -71,10 +71,10 @@ async Task AuthenticateSASL(List mechanisms, string username, bool async { // At the time of writing PostgreSQL only supports SCRAM-SHA-256 and SCRAM-SHA-256-PLUS var serverSupportsSha256 = mechanisms.Contains("SCRAM-SHA-256"); - var clientSupportsSha256 = serverSupportsSha256 && Settings.ChannelBinding != ChannelBinding.Require; + var allowSha256 = serverSupportsSha256 && Settings.ChannelBinding != ChannelBinding.Require; var serverSupportsSha256Plus = mechanisms.Contains("SCRAM-SHA-256-PLUS"); - var clientSupportsSha256Plus = serverSupportsSha256Plus && Settings.ChannelBinding != ChannelBinding.Disable; - if (!clientSupportsSha256 && !clientSupportsSha256Plus) + var allowSha256Plus = serverSupportsSha256Plus && Settings.ChannelBinding != ChannelBinding.Disable; + if (!allowSha256 && !allowSha256Plus) { if (serverSupportsSha256 && Settings.ChannelBinding == ChannelBinding.Require) throw new NpgsqlException($"Couldn't connect because {nameof(ChannelBinding)} is set to {nameof(ChannelBinding.Require)} " + @@ -92,10 +92,10 @@ async Task AuthenticateSASL(List mechanisms, string username, bool async var cbind = string.Empty; var successfulBind = false; - if (clientSupportsSha256Plus) + if (allowSha256Plus) DataSource.TransportSecurityHandler.AuthenticateSASLSha256Plus(this, ref mechanism, ref cbindFlag, ref cbind, ref successfulBind); - if (!successfulBind && serverSupportsSha256) + if (!successfulBind && allowSha256) { mechanism = "SCRAM-SHA-256"; // We can get here if PostgreSQL supports only SCRAM-SHA-256 or there was an error while binding to SCRAM-SHA-256-PLUS diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index b1a92df4b6..8208e7386c 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1658,12 +1658,12 @@ internal void ClearTransaction(Exception? disposeReason = null) internal bool IsSecure { get; private set; } /// - /// Returns whether SCRAM-SHA256 is being user for the connection + /// Returns whether SCRAM-SHA256 is being used for the connection /// internal bool IsScram { get; private set; } /// - /// Returns whether SCRAM-SHA256-PLUS is being user for the connection + /// Returns whether SCRAM-SHA256-PLUS is being used for the connection /// internal bool IsScramPlus { get; private set; } From 3146bcda307d49ecd21d9e73c87a142dd1ee8cba Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 24 Feb 2025 17:11:06 +0300 Subject: [PATCH 018/155] Add SHA3 hash algorithms for SASL authentication (#6028) Closes #6027 --------- Co-authored-by: Shay Rojansky --- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 74 ++++++++++----------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 7d53040bac..0d69907e7f 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -200,51 +200,47 @@ internal void AuthenticateSASLSha256Plus(ref string mechanism, ref string cbindF // But to be on the safe side we'll just create a new instance of it using var remoteCertificate = new X509Certificate2(sslStream.RemoteCertificate); // Checking for hashing algorithms - HashAlgorithm? hashAlgorithm = null; var algorithmName = remoteCertificate.SignatureAlgorithm.FriendlyName; - if (algorithmName is null) - { - ConnectionLogger.LogWarning("Signature algorithm was null, falling back to SCRAM-SHA-256"); - } - else if (algorithmName.StartsWith("sha1", StringComparison.OrdinalIgnoreCase) || - algorithmName.StartsWith("md5", StringComparison.OrdinalIgnoreCase) || - algorithmName.StartsWith("sha256", StringComparison.OrdinalIgnoreCase)) - { - hashAlgorithm = SHA256.Create(); - } - else if (algorithmName.StartsWith("sha384", StringComparison.OrdinalIgnoreCase)) - { - hashAlgorithm = SHA384.Create(); - } - else if (algorithmName.StartsWith("sha512", StringComparison.OrdinalIgnoreCase)) + + HashAlgorithm? hashAlgorithm = algorithmName switch { - hashAlgorithm = SHA512.Create(); - } - else + not null when algorithmName.StartsWith("sha1", StringComparison.OrdinalIgnoreCase) => SHA256.Create(), + not null when algorithmName.StartsWith("md5", StringComparison.OrdinalIgnoreCase) => SHA256.Create(), + not null when algorithmName.StartsWith("sha256", StringComparison.OrdinalIgnoreCase) => SHA256.Create(), + not null when algorithmName.StartsWith("sha384", StringComparison.OrdinalIgnoreCase) => SHA384.Create(), + not null when algorithmName.StartsWith("sha512", StringComparison.OrdinalIgnoreCase) => SHA512.Create(), + not null when algorithmName.StartsWith("sha3-256", StringComparison.OrdinalIgnoreCase) => SHA3_256.Create(), + not null when algorithmName.StartsWith("sha3-384", StringComparison.OrdinalIgnoreCase) => SHA3_384.Create(), + not null when algorithmName.StartsWith("sha3-512", StringComparison.OrdinalIgnoreCase) => SHA3_512.Create(), + + _ => null + }; + + if (hashAlgorithm is null) { ConnectionLogger.LogWarning( - $"Support for signature algorithm {algorithmName} is not yet implemented, falling back to SCRAM-SHA-256"); + algorithmName is null + ? "Signature algorithm was null, falling back to SCRAM-SHA-256" + : $"Support for signature algorithm {algorithmName} is not yet implemented, falling back to SCRAM-SHA-256"); + return; } - if (hashAlgorithm != null) - { - using var _ = hashAlgorithm; - - // RFC 5929 - mechanism = "SCRAM-SHA-256-PLUS"; - // PostgreSQL only supports tls-server-end-point binding - cbindFlag = "p=tls-server-end-point"; - // SCRAM-SHA-256-PLUS depends on using ssl stream, so it's fine - var cbindFlagBytes = Encoding.UTF8.GetBytes($"{cbindFlag},,"); - - var certificateHash = hashAlgorithm.ComputeHash(remoteCertificate.GetRawCertData()); - var cbindBytes = new byte[cbindFlagBytes.Length + certificateHash.Length]; - cbindFlagBytes.CopyTo(cbindBytes, 0); - certificateHash.CopyTo(cbindBytes, cbindFlagBytes.Length); - cbind = Convert.ToBase64String(cbindBytes); - successfulBind = true; - IsScramPlus = true; - } + using var _ = hashAlgorithm; + + // RFC 5929 + mechanism = "SCRAM-SHA-256-PLUS"; + // PostgreSQL only supports tls-server-end-point binding + cbindFlag = "p=tls-server-end-point"; + // SCRAM-SHA-256-PLUS depends on using ssl stream, so it's fine + var cbindFlagBytes = Encoding.UTF8.GetBytes($"{cbindFlag},,"); + + var certificateHash = hashAlgorithm.ComputeHash(remoteCertificate.GetRawCertData()); + var cbindBytes = new byte[cbindFlagBytes.Length + certificateHash.Length]; + cbindFlagBytes.CopyTo(cbindBytes, 0); + certificateHash.CopyTo(cbindBytes, cbindFlagBytes.Length); + cbind = Convert.ToBase64String(cbindBytes); + successfulBind = true; + IsScramPlus = true; } static byte[] Hi(string str, byte[] salt, int count) From 061a5f2059b7fb5132b0cc8bf332a2255dccef7c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 4 Mar 2025 09:46:11 +0100 Subject: [PATCH 019/155] Remove dotnet SDK version from CI (use global.json) (#6037) --- .github/workflows/build.yml | 8 -------- .github/workflows/codeql-analysis.yml | 3 --- .github/workflows/native-aot.yml | 7 ------- .github/workflows/rich-code-nav.yml | 3 --- global.json | 2 +- 5 files changed, 1 insertion(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8547e3b8d1..5162a2bb45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,6 @@ concurrency: cancel-in-progress: true env: - dotnet_sdk_version: '9.0.100' postgis_version: 3 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Windows comes with PG pre-installed, and defines the PGPASSWORD environment variable. Remove it as it interferes @@ -70,9 +69,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: | - ${{ env.dotnet_sdk_version }} - name: Build run: dotnet build -c ${{ matrix.config }} @@ -355,8 +351,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: ${{ env.dotnet_sdk_version }} - name: Pack run: dotnet pack Npgsql.sln --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true @@ -389,8 +383,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: ${{ env.dotnet_sdk_version }} - name: Pack run: dotnet pack Npgsql.sln --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index edfd1fbd0f..9fa5eeb8e1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,6 @@ concurrency: cancel-in-progress: true env: - dotnet_sdk_version: '9.0.100' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true jobs: @@ -67,8 +66,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: ${{ env.dotnet_sdk_version }} - name: Build run: dotnet build -c Release diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 78c713c45b..25514352ce 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -15,7 +15,6 @@ concurrency: cancel-in-progress: true env: - dotnet_sdk_version: '9.0.100' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true AOT_Compat: | param([string]$targetFramework) @@ -109,9 +108,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: | - ${{ env.dotnet_sdk_version }} - name: Write script run: echo "$AOT_Compat" > test-aot-compatibility.ps1 @@ -146,9 +142,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: | - ${{ env.dotnet_sdk_version }} - name: Start PostgreSQL run: | diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index e007cab721..b25a971133 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -4,7 +4,6 @@ on: workflow_dispatch: env: - dotnet_sdk_version: '9.0.100' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true jobs: @@ -25,8 +24,6 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.0 - with: - dotnet-version: ${{ env.dotnet_sdk_version }} - name: Build run: dotnet build Npgsql.sln --configuration Debug diff --git a/global.json b/global.json index 733b653c18..9f1e930171 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "9.0.200", "rollForward": "latestMajor", "allowPrerelease": false } From a4a7f609c07e5dd9a046c8bd9d20c208ad11a12c Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 4 Mar 2025 15:12:41 +0300 Subject: [PATCH 020/155] Add support for specifying allowed auth methods (#6036) Closes #6035 --- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 21 ++++ src/Npgsql/NpgsqlConnectionStringBuilder.cs | 100 +++++++++++++++ src/Npgsql/PostgresEnvironment.cs | 2 + src/Npgsql/PublicAPI.Unshipped.txt | 2 + test/Npgsql.Tests/ConnectionTests.cs | 133 ++++++++++++++++++++ test/Npgsql.Tests/SecurityTests.cs | 12 +- 6 files changed, 264 insertions(+), 6 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 0d69907e7f..8dc5231fd2 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -18,6 +18,12 @@ partial class NpgsqlConnector { async Task Authenticate(string username, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { + var requiredAuthModes = Settings.RequireAuthModes; + if (requiredAuthModes == default) + requiredAuthModes = NpgsqlConnectionStringBuilder.ParseAuthMode(PostgresEnvironment.RequireAuth); + + var authenticated = false; + while (true) { timeout.CheckAndApply(this); @@ -25,23 +31,30 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async, Canc switch (msg.AuthRequestType) { case AuthenticationRequestType.Ok: + // If we didn't complete authentication, check whether it's allowed + if (!authenticated) + ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.None); return; case AuthenticationRequestType.CleartextPassword: + ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.Password); await AuthenticateCleartext(username, async, cancellationToken).ConfigureAwait(false); break; case AuthenticationRequestType.MD5Password: + ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.MD5); await AuthenticateMD5(username, ((AuthenticationMD5PasswordMessage)msg).Salt, async, cancellationToken).ConfigureAwait(false); break; case AuthenticationRequestType.SASL: + ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.ScramSHA256); await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, username, async, cancellationToken).ConfigureAwait(false); break; case AuthenticationRequestType.GSS: case AuthenticationRequestType.SSPI: + ThrowIfNotAllowed(requiredAuthModes, msg.AuthRequestType == AuthenticationRequestType.GSS ? RequireAuthMode.GSS : RequireAuthMode.SSPI); await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, this).ConfigureAwait(false); return; @@ -51,6 +64,14 @@ await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, username, as default: throw new NotSupportedException($"Authentication method not supported (Received: {msg.AuthRequestType})"); } + + authenticated = true; + } + + static void ThrowIfNotAllowed(RequireAuthMode requiredAuthModes, RequireAuthMode requestedAuthMode) + { + if (!requiredAuthModes.HasFlag(requestedAuthMode)) + throw new NpgsqlException($"\"{requestedAuthMode}\" authentication method is not allowed. Allowed methods: {requiredAuthModes}"); } } diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index ca0629d7f7..f0b258d356 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -683,6 +683,70 @@ public ChannelBinding ChannelBinding } ChannelBinding _channelBinding; + /// + /// Controls the available authentication methods. + /// + [Category("Security")] + [Description("Controls the available authentication methods.")] + [DisplayName("Require Auth")] + [NpgsqlConnectionStringProperty] + public string? RequireAuth + { + get => _requireAuth; + set + { + RequireAuthModes = ParseAuthMode(value); + _requireAuth = value; + SetValue(nameof(RequireAuth), value); + } + } + string? _requireAuth; + + internal RequireAuthMode RequireAuthModes { get; private set; } + + internal static RequireAuthMode ParseAuthMode(string? value) + { + var modes = value?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (modes is not { Length: > 0 }) + return RequireAuthMode.All; + + var isNegative = false; + RequireAuthMode parsedModes = default; + for (var i = 0; i < modes.Length; i++) + { + var mode = modes[i]; + var modeToParse = mode.AsSpan(); + if (mode.StartsWith('!')) + { + if (i > 0 && !isNegative) + throw new ArgumentException("Mixing both positive and negative authentication methods is not supported"); + + modeToParse = modeToParse.Slice(1); + isNegative = true; + } + else + { + if (i > 0 && isNegative) + throw new ArgumentException("Mixing both positive and negative authentication methods is not supported"); + } + + // Explicitly disallow 'All' as libpq doesn't have it + if (!Enum.TryParse(modeToParse, out var parsedMode) || parsedMode == RequireAuthMode.All) + throw new ArgumentException($"Unable to parse authentication method \"{modeToParse}\""); + + parsedModes |= parsedMode; + } + + var allowedModes = isNegative + ? (RequireAuthMode)(RequireAuthMode.All - parsedModes) + : parsedModes; + + if (allowedModes == default) + throw new ArgumentException($"No authentication method is allowed. Check \"{nameof(RequireAuth)}\" in connection string."); + + return allowedModes; + } + #endregion #region Properties - Pooling @@ -1735,4 +1799,40 @@ enum ReplicationMode Logical } +/// +/// Specifies which authentication methods are supported. +/// +[Flags] +enum RequireAuthMode +{ + /// + /// Plaintext password. + /// + Password = 1, + /// + /// MD5 hashed password. + /// + MD5 = 2, + /// + /// Kerberos. + /// + GSS = 4, + /// + /// Windows SSPI. + /// + SSPI = 8, + /// + /// SASL. + /// + ScramSHA256 = 16, + /// + /// No authentication exchange. + /// + None = 32, + /// + /// All authentication methods. For internal use. + /// + All = Password | MD5 | GSS | SSPI | ScramSHA256 | None +} + #endregion diff --git a/src/Npgsql/PostgresEnvironment.cs b/src/Npgsql/PostgresEnvironment.cs index bacdd9bfde..389df7d085 100644 --- a/src/Npgsql/PostgresEnvironment.cs +++ b/src/Npgsql/PostgresEnvironment.cs @@ -50,6 +50,8 @@ internal static string? SslCertRootDefault internal static string? SslNegotiation => Environment.GetEnvironmentVariable("PGSSLNEGOTIATION"); + internal static string? RequireAuth => Environment.GetEnvironmentVariable("PGREQUIREAUTH"); + static string? GetHomeDir() => Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "APPDATA" : "HOME"); diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 10d2965ba0..2311f1eb30 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -3,6 +3,8 @@ abstract Npgsql.NpgsqlDataSource.Clear() -> void Npgsql.NpgsqlConnection.CloneWithAsync(string! connectionString, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Action? Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void +Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string? +Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.set -> void Npgsql.NpgsqlDataSourceBuilder.ConfigureTypeLoading(System.Action! configureAction) -> Npgsql.NpgsqlDataSourceBuilder! diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 15a550fe50..90dbd4ecf1 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -1679,6 +1679,139 @@ public async Task PhysicalConnectionInitializer_disposes_connection() #endregion Physical connection initialization + #region Require auth + + [Test] + public async Task Connect_with_any_auth() + { + await using var dataSource = CreateDataSource(csb => + { + csb.RequireAuth = $"{RequireAuthMode.Password},{RequireAuthMode.MD5},{RequireAuthMode.GSS},{RequireAuthMode.SSPI},{RequireAuthMode.ScramSHA256},{RequireAuthMode.None}"; + }); + await using var conn = await dataSource.OpenConnectionAsync(); + } + + [Test] + [NonParallelizable] // Sets environment variable + public async Task Connect_with_any_auth_env() + { + using var _ = SetEnvironmentVariable("PGREQUIREAUTH", $"{RequireAuthMode.Password},{RequireAuthMode.MD5},{RequireAuthMode.GSS},{RequireAuthMode.SSPI},{RequireAuthMode.ScramSHA256},{RequireAuthMode.None}"); + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + } + + [Test] + public async Task Connect_with_any_except_none_auth() + { + await using var dataSource = CreateDataSource(csb => + { + csb.RequireAuth = $"!{RequireAuthMode.None}"; + }); + await using var conn = await dataSource.OpenConnectionAsync(); + } + + [Test] + [NonParallelizable] // Sets environment variable + public async Task Connect_with_any_except_none_auth_env() + { + using var _ = SetEnvironmentVariable("PGREQUIREAUTH", $"!{RequireAuthMode.None}"); + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + } + + [Test] + public async Task Fail_connect_with_none_auth() + { + await using var dataSource = CreateDataSource(csb => + { + csb.RequireAuth = $"{RequireAuthMode.None}"; + }); + var ex = Assert.ThrowsAsync(async () => await dataSource.OpenConnectionAsync())!; + Assert.That(ex.Message, Does.Contain("authentication method is not allowed")); + } + + [Test] + [NonParallelizable] // Sets environment variable + public async Task Fail_connect_with_none_auth_env() + { + using var _ = SetEnvironmentVariable("PGREQUIREAUTH", $"{RequireAuthMode.None}"); + await using var dataSource = CreateDataSource(); + var ex = Assert.ThrowsAsync(async () => await dataSource.OpenConnectionAsync())!; + Assert.That(ex.Message, Does.Contain("authentication method is not allowed")); + } + + [Test] + public async Task Connect_with_md5_auth() + { + await using var dataSource = CreateDataSource(csb => + { + csb.RequireAuth = $"{RequireAuthMode.MD5}"; + }); + try + { + await using var conn = await dataSource.OpenConnectionAsync(); + } + catch (Exception e) when (!IsOnBuildServer) + { + Console.WriteLine(e); + Assert.Ignore("MD5 authentication doesn't seem to be set up"); + } + } + + [Test] + [NonParallelizable] // Sets environment variable + public async Task Connect_with_md5_auth_env() + { + using var _ = SetEnvironmentVariable("PGREQUIREAUTH", $"{RequireAuthMode.MD5}"); + await using var dataSource = CreateDataSource(); + try + { + await using var conn = await dataSource.OpenConnectionAsync(); + } + catch (Exception e) when (!IsOnBuildServer) + { + Console.WriteLine(e); + Assert.Ignore("MD5 authentication doesn't seem to be set up"); + } + } + + [Test] + public void Mixed_auth_methods_not_supported([Values( + $"{nameof(RequireAuthMode.ScramSHA256)},!{nameof(RequireAuthMode.None)}", + $"!{nameof(RequireAuthMode.ScramSHA256)},{nameof(RequireAuthMode.None)}")] + string authMethods) + { + var csb = new NpgsqlConnectionStringBuilder(); + Assert.Throws(() => csb.RequireAuth = authMethods); + } + + [Test] + public void Remove_all_auth_methods_throws() + { + var csb = new NpgsqlConnectionStringBuilder(); + Assert.Throws(() => + csb.RequireAuth = $"!{RequireAuthMode.Password},!{RequireAuthMode.MD5},!{RequireAuthMode.GSS},!{RequireAuthMode.SSPI},!{RequireAuthMode.ScramSHA256},!{RequireAuthMode.None}"); + } + + [Test] + public void Unknown_auth_method_throws() + { + var csb = new NpgsqlConnectionStringBuilder(); + Assert.Throws(() => csb.RequireAuth = "SuperSecure"); + } + + [Test] + public void Auth_methods_are_trimmed() + { + var csb = new NpgsqlConnectionStringBuilder + { + RequireAuth = $"{RequireAuthMode.Password} , {RequireAuthMode.MD5}" + }; + Assert.That(csb.RequireAuthModes, Is.EqualTo(RequireAuthMode.Password | RequireAuthMode.MD5)); + } + + #endregion Require auth + [Test] [NonParallelizable] // Modifies global database info factories [IssueLink("https://github.com/npgsql/npgsql/issues/4425")] diff --git a/test/Npgsql.Tests/SecurityTests.cs b/test/Npgsql.Tests/SecurityTests.cs index f6451a633f..13b7ca7495 100644 --- a/test/Npgsql.Tests/SecurityTests.cs +++ b/test/Npgsql.Tests/SecurityTests.cs @@ -13,27 +13,27 @@ namespace Npgsql.Tests; public class SecurityTests : TestBase { [Test, Description("Establishes an SSL connection, assuming a self-signed server certificate")] - public void Basic_ssl() + public async Task Basic_ssl() { - using var dataSource = CreateDataSource(csb => + await using var dataSource = CreateDataSource(csb => { csb.SslMode = SslMode.Require; }); - using var conn = dataSource.OpenConnection(); + await using var conn = await dataSource.OpenConnectionAsync(); Assert.That(conn.IsSecure, Is.True); } [Test, Description("Default user must run with md5 password encryption")] - public void Default_user_uses_md5_password() + public async Task Default_user_uses_md5_password() { if (!IsOnBuildServer) Assert.Ignore("Only executed in CI"); - using var dataSource = CreateDataSource(csb => + await using var dataSource = CreateDataSource(csb => { csb.SslMode = SslMode.Require; }); - using var conn = dataSource.OpenConnection(); + await using var conn = await dataSource.OpenConnectionAsync(); Assert.That(conn.IsScram, Is.False); Assert.That(conn.IsScramPlus, Is.False); } From 81e9c5808a279e82a8471c8757e08adbe9dcd14f Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 17 Mar 2025 10:34:47 +0100 Subject: [PATCH 021/155] Migrate to SLNX (#6053) --- .github/workflows/build.yml | 4 +- .github/workflows/rich-code-nav.yml | 2 +- Npgsql.sln | 204 ------------------ Npgsql.slnx | 35 +++ ...sln.DotSettings => Npgsql.slnx.DotSettings | 0 5 files changed, 38 insertions(+), 207 deletions(-) delete mode 100644 Npgsql.sln create mode 100644 Npgsql.slnx rename Npgsql.sln.DotSettings => Npgsql.slnx.DotSettings (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5162a2bb45..76e67b4513 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -353,7 +353,7 @@ jobs: uses: actions/setup-dotnet@v4.3.0 - name: Pack - run: dotnet pack Npgsql.sln --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true + run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true - name: Upload artifacts (nupkg) uses: actions/upload-artifact@v4 @@ -385,7 +385,7 @@ jobs: uses: actions/setup-dotnet@v4.3.0 - name: Pack - run: dotnet pack Npgsql.sln --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true + run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index b25a971133..0266f288ff 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-dotnet@v4.3.0 - name: Build - run: dotnet build Npgsql.sln --configuration Debug + run: dotnet build --configuration Debug shell: bash - name: Rich Navigation Indexing diff --git a/Npgsql.sln b/Npgsql.sln deleted file mode 100644 index 80ef02c3a8..0000000000 --- a/Npgsql.sln +++ /dev/null @@ -1,204 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28822.285 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{ED612DB1-AB32-4603-95E7-891BACA71C39}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql", "src\Npgsql\Npgsql.csproj", "{9D13B739-62B1-4190-B386-7A9547304EB3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.Tests", "test\Npgsql.Tests\Npgsql.Tests.csproj", "{E9C258D7-0D8E-4E6A-9857-5C6438591755}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.Benchmarks", "test\Npgsql.Benchmarks\Npgsql.Benchmarks.csproj", "{8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.Json.NET", "src\Npgsql.Json.NET\Npgsql.Json.NET.csproj", "{9CBE603F-6746-411D-A5FD-CB2C948CD7D0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.NodaTime", "src\Npgsql.NodaTime\Npgsql.NodaTime.csproj", "{D8DF12D6-FA70-4653-BD8F-C188944836DE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.PluginTests", "test\Npgsql.PluginTests\Npgsql.PluginTests.csproj", "{9BD7FC3D-6956-42A8-A586-2558C499EBA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.NetTopologySuite", "src\Npgsql.NetTopologySuite\Npgsql.NetTopologySuite.csproj", "{6CB12050-DC9B-4155-BADD-BFDD54CDD70F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.GeoJSON", "src\Npgsql.GeoJSON\Npgsql.GeoJSON.csproj", "{F7C53EBD-0075-474F-A083-419257D04080}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Npgsql.Specification.Tests", "test\Npgsql.Specification.Tests\Npgsql.Specification.Tests.csproj", "{A77E5FAF-D775-4AB4-8846-8965C2104E60}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{004A2E0F-D34A-44D4-8DF0-D2BC63B57073}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props - README.md = README.md - global.json = global.json - NuGet.config = NuGet.config - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.SourceGenerators", "src\Npgsql.SourceGenerators\Npgsql.SourceGenerators.csproj", "{63026A19-60B8-4906-81CB-216F30E8094B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.OpenTelemetry", "src\Npgsql.OpenTelemetry\Npgsql.OpenTelemetry.csproj", "{DA29F063-1828-47D8-B051-800AF7C9A0BE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github", "Github", "{BA7B6F53-D24D-45AC-927A-266857EA8D1E}" - ProjectSection(SolutionItems) = preProject - .github\workflows\build.yml = .github\workflows\build.yml - .github\dependabot.yml = .github\dependabot.yml - .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - .github\workflows\rich-code-nav.yml = .github\workflows\rich-code-nav.yml - .github\workflows\native-aot.yml = .github\workflows\native-aot.yml - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.DependencyInjection", "src\Npgsql.DependencyInjection\Npgsql.DependencyInjection.csproj", "{B58E12EB-E43D-4D77-894E-5157D2269836}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.DependencyInjection.Tests", "test\Npgsql.DependencyInjection.Tests\Npgsql.DependencyInjection.Tests.csproj", "{EB2530FC-69F7-4DCB-A8B3-3671A157ED32}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.NativeAotTests", "test\Npgsql.NativeAotTests\Npgsql.NativeAotTests.csproj", "{20F2E9D6-A69E-4BAE-9236-574B0AA59139}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9D13B739-62B1-4190-B386-7A9547304EB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Debug|x86.ActiveCfg = Debug|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Debug|x86.Build.0 = Debug|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Release|Any CPU.Build.0 = Release|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Release|x86.ActiveCfg = Release|Any CPU - {9D13B739-62B1-4190-B386-7A9547304EB3}.Release|x86.Build.0 = Release|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Debug|x86.ActiveCfg = Debug|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Debug|x86.Build.0 = Debug|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Release|Any CPU.Build.0 = Release|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Release|x86.ActiveCfg = Release|Any CPU - {E9C258D7-0D8E-4E6A-9857-5C6438591755}.Release|x86.Build.0 = Release|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Debug|x86.ActiveCfg = Debug|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Debug|x86.Build.0 = Debug|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Release|Any CPU.Build.0 = Release|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Release|x86.ActiveCfg = Release|Any CPU - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8}.Release|x86.Build.0 = Release|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Debug|x86.ActiveCfg = Debug|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Debug|x86.Build.0 = Debug|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Release|Any CPU.Build.0 = Release|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Release|x86.ActiveCfg = Release|Any CPU - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0}.Release|x86.Build.0 = Release|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Debug|x86.ActiveCfg = Debug|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Debug|x86.Build.0 = Debug|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Release|Any CPU.Build.0 = Release|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Release|x86.ActiveCfg = Release|Any CPU - {D8DF12D6-FA70-4653-BD8F-C188944836DE}.Release|x86.Build.0 = Release|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Debug|x86.ActiveCfg = Debug|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Debug|x86.Build.0 = Debug|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Release|Any CPU.Build.0 = Release|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Release|x86.ActiveCfg = Release|Any CPU - {9BD7FC3D-6956-42A8-A586-2558C499EBA2}.Release|x86.Build.0 = Release|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Debug|x86.ActiveCfg = Debug|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Debug|x86.Build.0 = Debug|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Release|Any CPU.Build.0 = Release|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Release|x86.ActiveCfg = Release|Any CPU - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F}.Release|x86.Build.0 = Release|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Debug|x86.ActiveCfg = Debug|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Debug|x86.Build.0 = Debug|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Release|Any CPU.Build.0 = Release|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Release|x86.ActiveCfg = Release|Any CPU - {F7C53EBD-0075-474F-A083-419257D04080}.Release|x86.Build.0 = Release|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Debug|x86.ActiveCfg = Debug|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Debug|x86.Build.0 = Debug|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Release|Any CPU.Build.0 = Release|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Release|x86.ActiveCfg = Release|Any CPU - {A77E5FAF-D775-4AB4-8846-8965C2104E60}.Release|x86.Build.0 = Release|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Debug|x86.ActiveCfg = Debug|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Debug|x86.Build.0 = Debug|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Release|Any CPU.Build.0 = Release|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Release|x86.ActiveCfg = Release|Any CPU - {63026A19-60B8-4906-81CB-216F30E8094B}.Release|x86.Build.0 = Release|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|x86.ActiveCfg = Debug|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Debug|x86.Build.0 = Debug|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Release|Any CPU.Build.0 = Release|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Release|x86.ActiveCfg = Release|Any CPU - {DA29F063-1828-47D8-B051-800AF7C9A0BE}.Release|x86.Build.0 = Release|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Debug|x86.ActiveCfg = Debug|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Debug|x86.Build.0 = Debug|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Release|Any CPU.Build.0 = Release|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Release|x86.ActiveCfg = Release|Any CPU - {B58E12EB-E43D-4D77-894E-5157D2269836}.Release|x86.Build.0 = Release|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Debug|x86.ActiveCfg = Debug|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Debug|x86.Build.0 = Debug|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Release|Any CPU.Build.0 = Release|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Release|x86.ActiveCfg = Release|Any CPU - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32}.Release|x86.Build.0 = Release|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Debug|x86.ActiveCfg = Debug|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Debug|x86.Build.0 = Debug|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Release|Any CPU.Build.0 = Release|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Release|x86.ActiveCfg = Release|Any CPU - {20F2E9D6-A69E-4BAE-9236-574B0AA59139}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {9D13B739-62B1-4190-B386-7A9547304EB3} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {E9C258D7-0D8E-4E6A-9857-5C6438591755} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {8B4AE9B6-CDAC-44DD-A5CD-28A470D363B8} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {9CBE603F-6746-411D-A5FD-CB2C948CD7D0} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {D8DF12D6-FA70-4653-BD8F-C188944836DE} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {9BD7FC3D-6956-42A8-A586-2558C499EBA2} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {6CB12050-DC9B-4155-BADD-BFDD54CDD70F} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {F7C53EBD-0075-474F-A083-419257D04080} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {A77E5FAF-D775-4AB4-8846-8965C2104E60} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {63026A19-60B8-4906-81CB-216F30E8094B} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {DA29F063-1828-47D8-B051-800AF7C9A0BE} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {BA7B6F53-D24D-45AC-927A-266857EA8D1E} = {004A2E0F-D34A-44D4-8DF0-D2BC63B57073} - {B58E12EB-E43D-4D77-894E-5157D2269836} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} - {EB2530FC-69F7-4DCB-A8B3-3671A157ED32} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {20F2E9D6-A69E-4BAE-9236-574B0AA59139} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C90AEECD-DB4C-4BE6-B506-16A449852FB8} - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = Npgsql.csproj - EndGlobalSection -EndGlobal diff --git a/Npgsql.slnx b/Npgsql.slnx new file mode 100644 index 0000000000..5404551fcd --- /dev/null +++ b/Npgsql.slnx @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Npgsql.sln.DotSettings b/Npgsql.slnx.DotSettings similarity index 100% rename from Npgsql.sln.DotSettings rename to Npgsql.slnx.DotSettings From eabc6ab1fc3ecd1b3b2dfd05da04f5ff3bdeb668 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 17 Mar 2025 13:21:45 +0100 Subject: [PATCH 022/155] Switch to Ubuntu 24.04 in CI (#6054) --- .github/workflows/build.yml | 17 ++++++++--------- .github/workflows/native-aot.yml | 4 ++-- .github/workflows/trigger-doc-build.yml | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76e67b4513..3d331bc5a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,12 +28,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04] + os: [ubuntu-24.04] pg_major: [17, 16, 15, 14, 13] config: [Release] test_tfm: [net8.0] include: - - os: ubuntu-22.04 + - os: ubuntu-24.04 pg_major: 17 config: Debug test_tfm: net8.0 @@ -45,7 +45,7 @@ jobs: pg_major: 17 config: Release test_tfm: net8.0 -# - os: ubuntu-22.04 +# - os: ubuntu-24.04 # pg_major: 17 # config: Release # test_tfm: net8.0 @@ -80,10 +80,9 @@ jobs: # First uninstall any PostgreSQL installed on the image dpkg-query -W --showformat='${Package}\n' 'postgresql-*' | xargs sudo dpkg -P postgresql - # Import the repository signing key - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - - - sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main ${{ matrix.pg_major }}" >> /etc/apt/sources.list.d/pgdg.list' + # Automated repository configuration + sudo apt install -y postgresql-common + sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo apt-get update -qq sudo apt-get install -qq postgresql-${{ matrix.pg_major }} export PGDATA=/etc/postgresql/${{ matrix.pg_major }}/main @@ -333,7 +332,7 @@ jobs: publish-ci: needs: build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: github.event_name == 'push' && github.repository == 'npgsql/npgsql' environment: myget @@ -373,7 +372,7 @@ jobs: release: needs: build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: github.event_name == 'push' && startsWith(github.repository, 'npgsql/') && needs.build.outputs.is_release == 'true' environment: nuget.org diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 25514352ce..0f18872275 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -87,7 +87,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-22.04 ] + os: [ ubuntu-24.04 ] pg_major: [ 15 ] tfm: [ net8.0 ] @@ -121,7 +121,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04] + os: [ubuntu-24.04] pg_major: [15] tfm: [ net8.0 ] diff --git a/.github/workflows/trigger-doc-build.yml b/.github/workflows/trigger-doc-build.yml index dfbe89601e..30c6b5fa62 100644 --- a/.github/workflows/trigger-doc-build.yml +++ b/.github/workflows/trigger-doc-build.yml @@ -10,7 +10,7 @@ on: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Trigger documentation build run: | From 562d3954042ae005fef0f9bac918786a51fe3cc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 00:43:41 +0100 Subject: [PATCH 023/155] Bump actions/setup-dotnet from 4.3.0 to 4.3.1 (#6059) --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/native-aot.yml | 4 ++-- .github/workflows/rich-code-nav.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d331bc5a8..192b5525d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Build run: dotnet build -c ${{ matrix.config }} @@ -349,7 +349,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Pack run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true @@ -381,7 +381,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Pack run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9fa5eeb8e1..2f34b67e27 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -65,7 +65,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Build run: dotnet build -c Release diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 0f18872275..cd6498fd24 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -107,7 +107,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Write script run: echo "$AOT_Compat" > test-aot-compatibility.ps1 @@ -141,7 +141,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Start PostgreSQL run: | diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index 0266f288ff..d0649277ca 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -23,7 +23,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.0 + uses: actions/setup-dotnet@v4.3.1 - name: Build run: dotnet build --configuration Debug From 2bc67c3e8cd1049a1af4f4dc64ad9fd121c21451 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 19 Mar 2025 17:04:39 +0100 Subject: [PATCH 024/155] Update copyright to 2025 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 50cb8595eb..de99b4fd8c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true true - Copyright 2024 © The Npgsql Development Team + Copyright 2025 © The Npgsql Development Team Npgsql PostgreSQL https://github.com/npgsql/npgsql From 22a0aa9bd0bf69bb3f915a0c1858680dc9087897 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 20 Mar 2025 12:55:20 +0300 Subject: [PATCH 025/155] Add basic testing for tracing (#6051) Closes #4285 --- src/Npgsql/Internal/NpgsqlConnector.cs | 1 - src/Npgsql/NpgsqlActivitySource.cs | 7 +- src/Npgsql/NpgsqlCommand.cs | 10 +- test/Npgsql.Tests/TracingTests.cs | 201 +++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 test/Npgsql.Tests/TracingTests.cs diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 8208e7386c..dffe542ff0 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1177,7 +1177,6 @@ async Task MultiplexingReadLoop() // We have a resultset for the command - hand back control to the command (which will // return it to the user) - command.TraceReceivedFirstResponse(DataSource.Configuration.TracingOptions); ReaderCompleted.Reset(); command.ExecutionCompletion.SetResult(this); diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index 667728a89a..ce762cc642 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -107,12 +107,13 @@ internal static void ReceivedFirstResponse(Activity activity, NpgsqlTracingOptio internal static void CommandStop(Activity activity) { - activity.SetTag("otel.status_code", "OK"); + activity.SetStatus(ActivityStatusCode.Ok); activity.Dispose(); } internal static void SetException(Activity activity, Exception ex, bool escaped = true) { + // TODO: We can instead use Activity.AddException whenever we start using .NET 9 var tags = new ActivityTagsCollection { { "exception.type", ex.GetType().FullName }, @@ -122,8 +123,8 @@ internal static void SetException(Activity activity, Exception ex, bool escaped }; var activityEvent = new ActivityEvent("exception", tags: tags); activity.AddEvent(activityEvent); - activity.SetTag("otel.status_code", "ERROR"); - activity.SetTag("otel.status_description", ex is PostgresException pgEx ? pgEx.SqlState : ex.Message); + var statusDescription = ex is PostgresException pgEx ? pgEx.SqlState : ex.Message; + activity.SetStatus(ActivityStatusCode.Error, statusDescription); activity.Dispose(); } } diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index f1ef8bb832..86748a1b16 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1597,6 +1597,8 @@ internal virtual async ValueTask ExecuteReader(bool async, Com connector.CurrentReader = reader; await reader.NextResultAsync(cancellationToken).ConfigureAwait(false); + TraceReceivedFirstResponse(connector.DataSource.Configuration.TracingOptions); + return reader; } } @@ -1718,12 +1720,12 @@ internal void TraceCommandStart(NpgsqlConnectionStringBuilder settings, NpgsqlTr ? tracingOptions.BatchFilter?.Invoke(WrappingBatch) ?? true : tracingOptions.CommandFilter?.Invoke(this) ?? true; - var spanName = WrappingBatch is not null - ? tracingOptions.BatchSpanNameProvider?.Invoke(WrappingBatch) - : tracingOptions.CommandSpanNameProvider?.Invoke(this); - if (enableTracing) { + var spanName = WrappingBatch is not null + ? tracingOptions.BatchSpanNameProvider?.Invoke(WrappingBatch) + : tracingOptions.CommandSpanNameProvider?.Invoke(this); + CurrentActivity = NpgsqlActivitySource.CommandStart( settings, WrappingBatch is not null ? GetBatchFullCommandText() : CommandText, diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs new file mode 100644 index 0000000000..e3ff4a7c34 --- /dev/null +++ b/test/Npgsql.Tests/TracingTests.cs @@ -0,0 +1,201 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Npgsql.Tests; + +[NonParallelizable] +public class TracingTests(MultiplexingMode multiplexingMode) : MultiplexingTestBase(multiplexingMode) +{ + [Test] + public async Task Basic([Values] bool async, [Values] bool batch) + { + if (IsMultiplexing && !async) + return; + + var activities = new List(); + + using var activityListener = new ActivityListener(); + activityListener.ShouldListenTo = source => source.Name == "Npgsql"; + activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; + activityListener.ActivityStopped = activity => activities.Add(activity); + ActivitySource.AddActivityListener(activityListener); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + await ExecuteScalar(conn, async, batch, "SELECT 42"); + + Assert.That(activities.Count, Is.EqualTo(1)); + var activity = activities[0]; + Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Ok)); + + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var firstResponseEvent = activity.Events.First(); + Assert.That(firstResponseEvent.Name, Is.EqualTo("received-first-response")); + + var expectedTagCount = conn.Settings.Port == 5432 ? 9 : 10; + Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); + + var queryTag = activity.TagObjects.First(x => x.Key == "db.statement"); + Assert.That(queryTag.Value, Is.EqualTo("SELECT 42")); + + var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); + Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + + var userTag = activity.TagObjects.First(x => x.Key == "db.user"); + Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + + var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); + Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); + + var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); + Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); + + if (!IsMultiplexing) + { + var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); + Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); + } + } + + [Test] + public async Task Error([Values] bool async, [Values] bool batch) + { + if (IsMultiplexing && !async) + return; + + var activities = new List(); + + using var activityListener = new ActivityListener(); + activityListener.ShouldListenTo = source => source.Name == "Npgsql"; + activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; + activityListener.ActivityStopped = activity => activities.Add(activity); + ActivitySource.AddActivityListener(activityListener); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + Assert.ThrowsAsync(async () => await ExecuteScalar(conn, async, batch, "SELECT * FROM non_existing_table")); + + Assert.That(activities.Count, Is.EqualTo(1)); + var activity = activities[0]; + Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); + Assert.That(activity.StatusDescription, Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var exceptionEvent = activity.Events.First(); + Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); + + Assert.That(exceptionEvent.Tags.Count(), Is.EqualTo(4)); + + var exceptionTypeTag = exceptionEvent.Tags.First(x => x.Key == "exception.type"); + Assert.That(exceptionTypeTag.Value, Is.EqualTo("Npgsql.PostgresException")); + + var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); + StringAssert.Contains("relation \"non_existing_table\" does not exist", (string)exceptionMessageTag.Value!); + + var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); + StringAssert.Contains("relation \"non_existing_table\" does not exist", (string)exceptionStacktraceTag.Value!); + + var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); + Assert.That(exceptionEscapedTag.Value, Is.True); + + var expectedTagCount = conn.Settings.Port == 5432 ? 9 : 10; + Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); + + var queryTag = activity.TagObjects.First(x => x.Key == "db.statement"); + Assert.That(queryTag.Value, Is.EqualTo("SELECT * FROM non_existing_table")); + + var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); + Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + + var userTag = activity.TagObjects.First(x => x.Key == "db.user"); + Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + + var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); + Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); + + var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); + Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); + + if (!IsMultiplexing) + { + var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); + Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); + } + } + + [Test] + public async Task Configure_tracing([Values] bool async, [Values] bool batch) + { + if (IsMultiplexing && !async) + return; + + var activities = new List(); + + using var activityListener = new ActivityListener(); + activityListener.ShouldListenTo = source => source.Name == "Npgsql"; + activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; + activityListener.ActivityStopped = activity => activities.Add(activity); + ActivitySource.AddActivityListener(activityListener); + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConfigureTracing(options => + { + options + .EnableFirstResponseEvent(enable: false) + .ConfigureCommandFilter(cmd => cmd.CommandText.Contains('2')) + .ConfigureBatchFilter(batch => batch.BatchCommands[0].CommandText.Contains('2')) + .ConfigureCommandSpanNameProvider(_ => "unknown_query") + .ConfigureBatchSpanNameProvider(_ => "unknown_query") + .ConfigureCommandEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")) + .ConfigureBatchEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")); + }); + await using var dataSource = dataSourceBuilder.Build(); + await using var conn = await dataSource.OpenConnectionAsync(); + + await ExecuteScalar(conn, async, batch, "SELECT 1"); + + Assert.That(activities.Count, Is.EqualTo(0)); + + await ExecuteScalar(conn, async, batch, "SELECT 2"); + + Assert.That(activities.Count, Is.EqualTo(1)); + var activity = activities[0]; + Assert.That(activity.DisplayName, Is.EqualTo("unknown_query")); + Assert.That(activity.OperationName, Is.EqualTo("unknown_query")); + + Assert.That(activity.Events.Count(), Is.EqualTo(0)); + + var customTag = activity.TagObjects.First(x => x.Key == "custom_tag"); + Assert.That(customTag.Value, Is.EqualTo("custom_value")); + } + + async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query) + { + if (!isBatch) + { + if (async) + return await connection.ExecuteScalarAsync(query); + else + return connection.ExecuteScalar(query); + } + else + { + await using var batch = connection.CreateBatch(); + var batchCommand = batch.CreateBatchCommand(); + batchCommand.CommandText = query; + batch.BatchCommands.Add(batchCommand); + + if (async) + return await batch.ExecuteScalarAsync(); + else + return batch.ExecuteScalar(); + } + } +} From e8ce19fe2aea5df32f508ba2b1cc15a1307d1a22 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 22 Mar 2025 15:03:15 +0000 Subject: [PATCH 026/155] NpgsqlParameterCollection.Clone() should set correct collection instance (#6066) fix #6065 --- src/Npgsql/NpgsqlParameterCollection.cs | 2 +- test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlParameterCollection.cs b/src/Npgsql/NpgsqlParameterCollection.cs index b2c56d7ac7..2e6b0f5012 100644 --- a/src/Npgsql/NpgsqlParameterCollection.cs +++ b/src/Npgsql/NpgsqlParameterCollection.cs @@ -653,7 +653,7 @@ internal void CloneTo(NpgsqlParameterCollection other) foreach (var param in InternalList) { var newParam = param.Clone(); - newParam.Collection = this; + newParam.Collection = other; other.InternalList.Add(newParam); } diff --git a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs index 6c09b7b708..e2c7ba364c 100644 --- a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs @@ -4,6 +4,7 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace Npgsql.Tests; @@ -320,6 +321,18 @@ public void Clean_name() Assert.AreEqual(NpgsqlParameter.PositionalName, param.ParameterName); } + [Test] + public void Clone_sets_correct_collection() + { + var cmd = new NpgsqlCommand(); + cmd.Parameters.Add(new NpgsqlParameter { TypedValue = 42 }); + Assert.AreSame(cmd.Parameters, cmd.Parameters.Single().Collection); + + cmd = cmd.Clone(); + Assert.AreSame(cmd.Parameters, cmd.Parameters.Single().Collection); + } + + public NpgsqlParameterCollectionTests(CompatMode compatMode) { _compatMode = compatMode; From aaf92983c9305f10e5892ff815c73b49491d7f0e Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sat, 22 Mar 2025 16:07:09 +0100 Subject: [PATCH 027/155] Fix brew on mac CI (#6071) --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 192b5525d4..2bdd30de10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -232,6 +232,7 @@ jobs: - name: Start PostgreSQL ${{ matrix.pg_major }} (MacOS) if: startsWith(matrix.os, 'macos') run: | + brew update brew install postgresql@${{ matrix.pg_major }} PGDATA=/opt/homebrew/var/postgresql@${{ matrix.pg_major }} From ef219b73f12b040010edb8362522db661a5f5969 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 26 Mar 2025 14:17:51 +0300 Subject: [PATCH 028/155] Fix adding to hash lookup while renaming an unnamed parameter (#6073) Fixes #6067 --- src/Npgsql/NpgsqlParameterCollection.cs | 2 +- .../NpgsqlParameterCollectionTests.cs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlParameterCollection.cs b/src/Npgsql/NpgsqlParameterCollection.cs index 2e6b0f5012..106f681f0b 100644 --- a/src/Npgsql/NpgsqlParameterCollection.cs +++ b/src/Npgsql/NpgsqlParameterCollection.cs @@ -143,7 +143,7 @@ internal void ChangeParameterName(NpgsqlParameter parameter, string? value) var oldTrimmedName = parameter.TrimmedName; parameter.ChangeParameterName(value); - if (_caseInsensitiveLookup is null || _caseInsensitiveLookup.Count == 0) + if (_caseInsensitiveLookup is null) return; var index = IndexOf(parameter); diff --git a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs index e2c7ba364c..f6a188817b 100644 --- a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs @@ -71,6 +71,34 @@ public void Hash_lookup_parameter_rename_bug() Assert.That(command.Parameters.IndexOf("a_new_name"), Is.GreaterThanOrEqualTo(0)); } + [Test] + [IssueLink("https://github.com/npgsql/npgsql/issues/6067")] + public void Hash_lookup_unnamed_parameter_rename_bug() + { + if (_compatMode == CompatMode.TwoPass) + return; + + using var command = new NpgsqlCommand(); + + for (var i = 0; i < 3; i++) + { + // Put plenty of parameters in the collection to turn on hash lookup functionality. + for (var j = 0; j < LookupThreshold; j++) + { + // Create and add an unnamed parameter before renaming it + var parameter = command.CreateParameter(); + command.Parameters.Add(parameter); + parameter.ParameterName = $"{j}"; + } + + // Make sure hash lookup is generated. + Assert.AreEqual(command.Parameters["3"].ParameterName, "3"); + + // Remove all parameters to clear hash lookup + command.Parameters.Clear(); + } + } + [Test] public void Remove_duplicate_parameter([Values(LookupThreshold, LookupThreshold - 2)] int count) { From cf9d2433bc653f363705296b5804a9658bb49083 Mon Sep 17 00:00:00 2001 From: kurnakovv <59327306+kurnakovv@users.noreply.github.com> Date: Sun, 30 Mar 2025 17:08:53 +0900 Subject: [PATCH 029/155] Update LICENSE date (2024 -> 2025) (#6082) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a74ee166ce..c551cb7b0c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2002-2024, Npgsql +Copyright (c) 2002-2025, Npgsql Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement From 251d73b01e05d5a125588a9e60ac4d7bec012cd9 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 9 Apr 2025 15:09:29 +0300 Subject: [PATCH 030/155] Add tracing for physical connection open (#6091) Closes #4136 --- src/Npgsql/Internal/NpgsqlConnector.cs | 31 ++++- src/Npgsql/MultiplexingDataSource.cs | 27 +++- src/Npgsql/NpgsqlActivitySource.cs | 18 ++- src/Npgsql/NpgsqlCommand.cs | 2 +- src/Npgsql/NpgsqlTracingOptionsBuilder.cs | 15 ++- src/Npgsql/PublicAPI.Unshipped.txt | 1 + test/Npgsql.Tests/TracingTests.cs | 152 +++++++++++++++++++++- 7 files changed, 228 insertions(+), 18 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index dffe542ff0..a926fb1ca2 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -485,9 +485,18 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca LogMessages.OpeningPhysicalConnection(ConnectionLogger, Host, Port, Database, UserFacingConnectionString); var startOpenTimestamp = Stopwatch.GetTimestamp(); + Activity? activity = null; + try { - await OpenCore(this, Settings.SslMode, timeout, async, cancellationToken).ConfigureAwait(false); + var username = await GetUsernameAsync(async, cancellationToken).ConfigureAwait(false); + + activity = NpgsqlActivitySource.ConnectionOpen(this); + + await OpenCore(this, username, Settings.SslMode, timeout, async, cancellationToken).ConfigureAwait(false); + + if (activity is not null) + NpgsqlActivitySource.Enrich(activity, this); await DataSource.Bootstrap(this, timeout, forceReload: false, async, cancellationToken).ConfigureAwait(false); @@ -510,6 +519,8 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca // It is intentionally not awaited and will run as long as the connector is alive. // The CommandsInFlightWriter channel is completed in Cleanup, which should cause this task // to complete. + // Make sure we do not flow AsyncLocals like Activity.Current + using var __ = ExecutionContext.SuppressFlow(); _ = Task.Run(MultiplexingReadLoop, CancellationToken.None) .ContinueWith(t => { @@ -540,7 +551,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca { if (async) await DataSource.ConnectionInitializerAsync(tempConnection).ConfigureAwait(false); - else if (!async) + else DataSource.ConnectionInitializer(tempConnection); } finally @@ -553,17 +564,24 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca } } + if (activity is not null) + NpgsqlActivitySource.CommandStop(activity); + LogMessages.OpenedPhysicalConnection( - ConnectionLogger, Host, Port, Database, UserFacingConnectionString, (long)Stopwatch.GetElapsedTime(startOpenTimestamp).TotalMilliseconds, Id); + ConnectionLogger, Host, Port, Database, UserFacingConnectionString, + (long)Stopwatch.GetElapsedTime(startOpenTimestamp).TotalMilliseconds, Id); } catch (Exception e) { + if (activity is not null) + NpgsqlActivitySource.SetException(activity, e); Break(e); throw; } static async Task OpenCore( NpgsqlConnector conn, + string username, SslMode sslMode, NpgsqlTimeout timeout, bool async, @@ -571,8 +589,6 @@ static async Task OpenCore( { await conn.RawOpen(sslMode, timeout, async, cancellationToken).ConfigureAwait(false); - var username = await conn.GetUsernameAsync(async, cancellationToken).ConfigureAwait(false); - timeout.CheckAndApply(conn); conn.WriteStartupMessage(username); await conn.Flush(async, cancellationToken).ConfigureAwait(false); @@ -595,6 +611,7 @@ static async Task OpenCore( // If Allow was specified and we failed (without SSL), retry with SSL await OpenCore( conn, + username, sslMode == SslMode.Prefer ? SslMode.Disable : SslMode.Require, timeout, async, @@ -754,6 +771,8 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat else Connect(timeout); + ConnectionLogger.LogTrace("Socket connected to {Host}:{Port}", Host, Port); + _baseStream = new NetworkStream(_socket, true); _stream = _baseStream; @@ -810,8 +829,6 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat if (ReadBuffer.ReadBytesLeft > 0) throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); } - - ConnectionLogger.LogTrace("Socket connected to {Host}:{Port}", Host, Port); } catch { diff --git a/src/Npgsql/MultiplexingDataSource.cs b/src/Npgsql/MultiplexingDataSource.cs index 74c32b8c6f..60ba882923 100644 --- a/src/Npgsql/MultiplexingDataSource.cs +++ b/src/Npgsql/MultiplexingDataSource.cs @@ -56,6 +56,8 @@ internal MultiplexingDataSource( _connectionLogger = dataSourceConfig.LoggingConfiguration.ConnectionLogger; _commandLogger = dataSourceConfig.LoggingConfiguration.CommandLogger; + // Make sure we do not flow AsyncLocals like Activity.Current + using var _ = ExecutionContext.SuppressFlow(); _multiplexWriteLoop = Task.Run(MultiplexingWriteLoop, CancellationToken.None) .ContinueWith(t => { @@ -106,15 +108,28 @@ async Task MultiplexingWriteLoop() break; } - connector = await OpenNewConnector( - command.InternalConnection!, - new NpgsqlTimeout(TimeSpan.FromSeconds(Settings.Timeout)), - async: true, - CancellationToken.None).ConfigureAwait(false); + // At no point should we ever have an activity here + Debug.Assert(Activity.Current is null); + // Set current activity as the one from the command + // So child activities from physical open are bound to it + Activity.Current = command.CurrentActivity; + + try + { + connector = await OpenNewConnector( + command.InternalConnection!, + new NpgsqlTimeout(TimeSpan.FromSeconds(Settings.Timeout)), + async: true, + CancellationToken.None).ConfigureAwait(false); + } + finally + { + Activity.Current = null; + } if (connector != null) { - // Managed to created a new connector + // Managed to create a new connector connector.Connection = null; // See increment under over-capacity mode below diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index ce762cc642..4493bb272a 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -9,7 +9,7 @@ namespace Npgsql; static class NpgsqlActivitySource { - static readonly ActivitySource Source = new("Npgsql", "0.1.0"); + static readonly ActivitySource Source = new("Npgsql", "0.2.0"); internal static bool IsEnabled => Source.HasListeners(); @@ -61,6 +61,22 @@ static class NpgsqlActivitySource return activity; } + internal static Activity? ConnectionOpen(NpgsqlConnector connector) + { + if (!connector.DataSource.Configuration.TracingOptions.EnablePhysicalOpenTracing) + return null; + + var dbName = connector.Settings.Database ?? connector.InferredUserName; + var activity = Source.StartActivity(dbName, ActivityKind.Client); + if (activity is not { IsAllDataRequested: true }) + return activity; + + activity.SetTag("db.system", "postgresql"); + activity.SetTag("db.connection_string", connector.UserFacingConnectionString); + + return activity; + } + internal static void Enrich(Activity activity, NpgsqlConnector connector) { if (!activity.IsAllDataRequested) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 86748a1b16..71a646e262 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -51,7 +51,7 @@ public class NpgsqlCommand : DbCommand, ICloneable, IComponent internal List InternalBatchCommands { get; } - Activity? CurrentActivity; + internal Activity? CurrentActivity { get; private set; } /// /// Returns details about each statement that this command has executed. diff --git a/src/Npgsql/NpgsqlTracingOptionsBuilder.cs b/src/Npgsql/NpgsqlTracingOptionsBuilder.cs index 1da344553f..f81f4e7139 100644 --- a/src/Npgsql/NpgsqlTracingOptionsBuilder.cs +++ b/src/Npgsql/NpgsqlTracingOptionsBuilder.cs @@ -15,6 +15,7 @@ public sealed class NpgsqlTracingOptionsBuilder Func? _commandSpanNameProvider; Func? _batchSpanNameProvider; bool _enableFirstResponseEvent = true; + bool _enablePhysicalOpenTracing = true; internal NpgsqlTracingOptionsBuilder() { @@ -88,6 +89,16 @@ public NpgsqlTracingOptionsBuilder EnableFirstResponseEvent(bool enable = true) return this; } + /// + /// Gets or sets a value indicating whether to trace physical connection open. + /// Default is true to preserve existing behavior. + /// + public NpgsqlTracingOptionsBuilder EnablePhysicalOpenTracing(bool enable = true) + { + _enablePhysicalOpenTracing = enable; + return this; + } + internal NpgsqlTracingOptions Build() => new() { CommandFilter = _commandFilter, @@ -96,7 +107,8 @@ public NpgsqlTracingOptionsBuilder EnableFirstResponseEvent(bool enable = true) BatchEnrichmentCallback = _batchEnrichmentCallback, CommandSpanNameProvider = _commandSpanNameProvider, BatchSpanNameProvider = _batchSpanNameProvider, - EnableFirstResponseEvent = _enableFirstResponseEvent + EnableFirstResponseEvent = _enableFirstResponseEvent, + EnablePhysicalOpenTracing = _enablePhysicalOpenTracing }; } @@ -109,4 +121,5 @@ sealed class NpgsqlTracingOptions internal Func? CommandSpanNameProvider { get; init; } internal Func? BatchSpanNameProvider { get; init; } internal bool EnableFirstResponseEvent { get; init; } + internal bool EnablePhysicalOpenTracing { get; init; } } diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 2311f1eb30..47198a4165 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -38,6 +38,7 @@ Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCommandEnrichmentCallback(System.Act Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCommandFilter(System.Func? commandFilter) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCommandSpanNameProvider(System.Func? commandSpanNameProvider) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.EnableFirstResponseEvent(bool enable = true) -> Npgsql.NpgsqlTracingOptionsBuilder! +Npgsql.NpgsqlTracingOptionsBuilder.EnablePhysicalOpenTracing(bool enable = true) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTypeLoadingOptionsBuilder Npgsql.NpgsqlTypeLoadingOptionsBuilder.EnableTableCompositesLoading(bool enable = true) -> Npgsql.NpgsqlTypeLoadingOptionsBuilder! Npgsql.NpgsqlTypeLoadingOptionsBuilder.EnableTypeLoading(bool enable = true) -> Npgsql.NpgsqlTypeLoadingOptionsBuilder! diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index e3ff4a7c34..5cf0fca200 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net.Sockets; using System.Threading.Tasks; using NUnit.Framework; @@ -10,7 +11,80 @@ namespace Npgsql.Tests; public class TracingTests(MultiplexingMode multiplexingMode) : MultiplexingTestBase(multiplexingMode) { [Test] - public async Task Basic([Values] bool async, [Values] bool batch) + public async Task Basic_open([Values] bool async) + { + if (IsMultiplexing && !async) + return; + + var activities = new List(); + + using var activityListener = new ActivityListener(); + activityListener.ShouldListenTo = source => source.Name == "Npgsql"; + activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; + activityListener.ActivityStopped = activity => activities.Add(activity); + ActivitySource.AddActivityListener(activityListener); + + await using var dataSource = CreateDataSource(); + await using var conn = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); + + Assert.That(activities.Count, Is.EqualTo(1)); + ValidateActivity(activities[0], conn, IsMultiplexing); + + if (!IsMultiplexing) + return; + + activities.Clear(); + + // For multiplexing, we clear the pool to force next query to open another physical connection + dataSource.Clear(); + + await conn.ExecuteScalarAsync("SELECT 1"); + + Assert.That(activities.Count, Is.EqualTo(2)); + ValidateActivity(activities[0], conn, IsMultiplexing); + + // For multiplexing, query's activity can be considered as a parent for physical open's activity + Assert.That(activities[0].Parent, Is.SameAs(activities[1])); + + static void ValidateActivity(Activity activity, NpgsqlConnection conn, bool isMultiplexing) + { + Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Ok)); + + Assert.That(activity.Events.Count(), Is.EqualTo(0)); + + var expectedTagCount = conn.Settings.Port == 5432 ? 8 : 9; + Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); + + Assert.IsFalse(activity.TagObjects.Any(x => x.Key == "db.statement")); + + var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); + Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + + var userTag = activity.TagObjects.First(x => x.Key == "db.user"); + Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + + var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); + Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); + + var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); + Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); + + if (!isMultiplexing) + { + var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); + Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); + } + else + Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + } + } + + [Test] + public async Task Basic_query([Values] bool async, [Values] bool batch) { if (IsMultiplexing && !async) return; @@ -25,6 +99,11 @@ public async Task Basic([Values] bool async, [Values] bool batch) await using var dataSource = CreateDataSource(); await using var conn = await dataSource.OpenConnectionAsync(); + + // We're not interested in physical open's activity + Assert.That(activities.Count, Is.EqualTo(1)); + activities.Clear(); + await ExecuteScalar(conn, async, batch, "SELECT 42"); Assert.That(activities.Count, Is.EqualTo(1)); @@ -60,10 +139,68 @@ public async Task Basic([Values] bool async, [Values] bool batch) var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); } + else + Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); } [Test] - public async Task Error([Values] bool async, [Values] bool batch) + public async Task Error_open([Values] bool async) + { + if (IsMultiplexing && !async) + return; + + var activities = new List(); + + using var activityListener = new ActivityListener(); + activityListener.ShouldListenTo = source => source.Name == "Npgsql"; + activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; + activityListener.ActivityStopped = activity => activities.Add(activity); + ActivitySource.AddActivityListener(activityListener); + + await using var dataSource = CreateDataSource(x => x.Host = "not-existing-host"); + var ex = Assert.ThrowsAsync(async () => + { + await using var conn = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); + })!; + + Assert.That(activities.Count, Is.EqualTo(1)); + var activity = activities[0]; + Assert.That(activity.DisplayName, Is.EqualTo(dataSource.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo(dataSource.Settings.Database)); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); + Assert.That(activity.StatusDescription, Is.EqualTo(ex.Message)); + + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var exceptionEvent = activity.Events.First(); + Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); + + Assert.That(exceptionEvent.Tags.Count(), Is.EqualTo(4)); + + var exceptionTypeTag = exceptionEvent.Tags.First(x => x.Key == "exception.type"); + Assert.That(exceptionTypeTag.Value, Is.EqualTo(ex.GetType().FullName)); + + var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); + StringAssert.Contains(ex.Message, (string)exceptionMessageTag.Value!); + + var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); + StringAssert.Contains(ex.Message, (string)exceptionStacktraceTag.Value!); + + var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); + Assert.That(exceptionEscapedTag.Value, Is.True); + + Assert.That(activity.TagObjects.Count(), Is.EqualTo(2)); + + var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); + Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + + var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); + Assert.That(connStringTag.Value, Is.EqualTo(dataSource.ConnectionString)); + } + + [Test] + public async Task Error_query([Values] bool async, [Values] bool batch) { if (IsMultiplexing && !async) return; @@ -78,6 +215,11 @@ public async Task Error([Values] bool async, [Values] bool batch) await using var dataSource = CreateDataSource(); await using var conn = await dataSource.OpenConnectionAsync(); + + // We're not interested in physical open's activity + Assert.That(activities.Count, Is.EqualTo(1)); + activities.Clear(); + Assert.ThrowsAsync(async () => await ExecuteScalar(conn, async, batch, "SELECT * FROM non_existing_table")); Assert.That(activities.Count, Is.EqualTo(1)); @@ -128,6 +270,8 @@ public async Task Error([Values] bool async, [Values] bool batch) var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); } + else + Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); } [Test] @@ -148,6 +292,7 @@ public async Task Configure_tracing([Values] bool async, [Values] bool batch) dataSourceBuilder.ConfigureTracing(options => { options + .EnablePhysicalOpenTracing(enable: false) .EnableFirstResponseEvent(enable: false) .ConfigureCommandFilter(cmd => cmd.CommandText.Contains('2')) .ConfigureBatchFilter(batch => batch.BatchCommands[0].CommandText.Contains('2')) @@ -159,6 +304,9 @@ public async Task Configure_tracing([Values] bool async, [Values] bool batch) await using var dataSource = dataSourceBuilder.Build(); await using var conn = await dataSource.OpenConnectionAsync(); + // We disabled physical open tracing + Assert.That(activities.Count, Is.EqualTo(0)); + await ExecuteScalar(conn, async, batch, "SELECT 1"); Assert.That(activities.Count, Is.EqualTo(0)); From 42e02af2c98033d72dc8fc3a12404e2b46c1cc0b Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Fri, 11 Apr 2025 17:57:07 +0300 Subject: [PATCH 031/155] Start testing on .NET 9 (#5945) Start testing on .NET 9 --- .github/workflows/build.yml | 10 +++++++--- .github/workflows/native-aot.yml | 8 ++++---- test/MStatDumper/MStatDumper.csproj | 2 +- .../Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj | 1 + test/Npgsql.Tests/ExceptionTests.cs | 2 ++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bdd30de10..6c62460816 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,17 +31,21 @@ jobs: os: [ubuntu-24.04] pg_major: [17, 16, 15, 14, 13] config: [Release] - test_tfm: [net8.0] + test_tfm: [net9.0] include: - os: ubuntu-24.04 pg_major: 17 config: Debug - test_tfm: net8.0 + test_tfm: net9.0 - os: macos-15 pg_major: 16 config: Release - test_tfm: net8.0 + test_tfm: net9.0 - os: windows-2022 + pg_major: 17 + config: Release + test_tfm: net9.0 + - os: ubuntu-24.04 pg_major: 17 config: Release test_tfm: net8.0 diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index cd6498fd24..b3b2346a35 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -89,7 +89,7 @@ jobs: matrix: os: [ ubuntu-24.04 ] pg_major: [ 15 ] - tfm: [ net8.0 ] + tfm: [ net9.0 ] steps: - name: Checkout @@ -123,7 +123,7 @@ jobs: matrix: os: [ubuntu-24.04] pg_major: [15] - tfm: [ net8.0 ] + tfm: [ net9.0 ] steps: - name: Checkout @@ -163,11 +163,11 @@ jobs: - name: Write binary size to summary run: | - size="$(ls -l test/Npgsql.NativeAotTests/bin/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests | cut -d ' ' -f 5)" + size="$(ls -l test/Npgsql.NativeAotTests/bin/Release/net9.0/linux-x64/native/Npgsql.NativeAotTests | cut -d ' ' -f 5)" echo "Binary size is $size bytes ($((size / (1024 * 1024))) mb)" >> $GITHUB_STEP_SUMMARY - name: Dump mstat - run: dotnet run --project test/MStatDumper/MStatDumper.csproj -c release -f ${{ matrix.tfm }} -- "test/Npgsql.NativeAotTests/obj/Release/net8.0/linux-x64/native/Npgsql.NativeAotTests.mstat" md >> $GITHUB_STEP_SUMMARY + run: dotnet run --project test/MStatDumper/MStatDumper.csproj -c release -f ${{ matrix.tfm }} -- "test/Npgsql.NativeAotTests/obj/Release/net9.0/linux-x64/native/Npgsql.NativeAotTests.mstat" md >> $GITHUB_STEP_SUMMARY - name: Upload mstat uses: actions/upload-artifact@v4 diff --git a/test/MStatDumper/MStatDumper.csproj b/test/MStatDumper/MStatDumper.csproj index 3cab4d57fd..456bd1f3b9 100644 --- a/test/MStatDumper/MStatDumper.csproj +++ b/test/MStatDumper/MStatDumper.csproj @@ -3,7 +3,7 @@ Exe - net8.0 + net9.0 enable disable diff --git a/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj b/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj index 31831faded..0757fb0dd6 100644 --- a/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj +++ b/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj @@ -9,6 +9,7 @@ true false true + false diff --git a/test/Npgsql.Tests/ExceptionTests.cs b/test/Npgsql.Tests/ExceptionTests.cs index 21d83ff9fb..b58617ef52 100644 --- a/test/Npgsql.Tests/ExceptionTests.cs +++ b/test/Npgsql.Tests/ExceptionTests.cs @@ -210,6 +210,7 @@ public void NpgsqlException_IsTransient() Assert.False(new NpgsqlException("", new Exception("Inner Exception")).IsTransient); } +#if !NET9_0_OR_GREATER #pragma warning disable SYSLIB0051 #pragma warning disable 618 [Test] @@ -309,4 +310,5 @@ public void Base_exception_property_serialization() Assert.That(ex.StackTrace, Is.EqualTo(info.GetValue("StackTraceString", typeof(string)))); } #pragma warning restore SYSLIB0051 +#endif } From 7ad12a45c0f76e540ab1ab9a32bebfb915c87c05 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 27 Apr 2025 12:22:42 +0200 Subject: [PATCH 032/155] Turn on (#6097) --- src/Directory.Build.props | 1 + src/Npgsql/Npgsql.csproj | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b94a8a91bd..fdf88bb904 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,7 @@ + true true diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index a28f47cfbe..e173d25794 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -19,10 +19,6 @@ - - - - From 3f89c1df4c5756a014a87e2655fd38bde7eace85 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 28 Apr 2025 17:49:24 +0200 Subject: [PATCH 033/155] Reenable public API analyzer (#6101) --- src/Npgsql/Npgsql.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index e173d25794..d4376c7774 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -13,6 +13,7 @@ + From c243c4f0b7c458208f5224f3cc5320fcf0dbc6e0 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 28 Apr 2025 18:49:53 +0300 Subject: [PATCH 034/155] Update Npgsql to .NET 9 (#6099) --- src/Npgsql/Internal/NpgsqlConnector.cs | 16 ++++++++++++++++ src/Npgsql/Npgsql.csproj | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index a926fb1ca2..7c231af096 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -881,11 +881,20 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, // Windows crypto API has a bug with pem certs // See #3650 using var previousCert = cert; +#if NET9_0_OR_GREATER + cert = X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null); +#else cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12)); +#endif } } +#if NET9_0_OR_GREATER + // If it's null, it's probably PFX + cert ??= X509CertificateLoader.LoadPkcs12FromFile(certPath, password); +#else cert ??= new X509Certificate2(certPath, password); +#endif clientCertificates.Add(cert); _certificate = cert; @@ -1727,7 +1736,14 @@ static RemoteCertificateValidationCallback SslRootValidation(bool verifyFull, st certs.ImportFromPemFile(certRootPath); if (certs.Count == 0) + { +#if NET9_0_OR_GREATER + // This is not a PEM certificate, probably PFX + certs.Add(X509CertificateLoader.LoadPkcs12FromFile(certRootPath, null)); +#else certs.Add(new X509Certificate2(certRootPath)); +#endif + } } chain.ChainPolicy.CustomTrustStore.AddRange(certs); diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index d4376c7774..096b6c762e 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -5,7 +5,7 @@ Npgsql is the open source .NET data provider for PostgreSQL. npgsql;postgresql;postgres;ado;ado.net;database;sql README.md - net8.0 + net8.0;net9.0 $(NoWarn);CA2017 $(NoWarn);NPG9001 $(NoWarn);NPG9002 From 73202604c3d66d4b55c81d3d532bf0e78cc5c2ee Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 30 Apr 2025 19:52:50 +0300 Subject: [PATCH 035/155] Ignore system CA store if root certificate is provided (#6102) Closes #6100 --- src/Npgsql/Internal/NpgsqlConnector.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 7c231af096..6a3b71d576 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1710,13 +1710,8 @@ static RemoteCertificateValidationCallback SslRootValidation(bool verifyFull, st if (certificate is null || chain is null) return false; - // No errors here - no reason to check further - if (sslPolicyErrors == SslPolicyErrors.None) - return true; - - // That's VerifyCA check and the only error is name mismatch - no reason to check further - if (!verifyFull && sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch) - return true; + // Even if there was no error while validating, we have to check one more time with the provided certificate + // As this is the exact same behavior as libpq // That's VerifyFull check and we have name mismatch - no reason to check further if (verifyFull && sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) From 4da52a03f441f23a4f5597ac106b7833ab4fbe90 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 7 May 2025 13:49:01 +0300 Subject: [PATCH 036/155] Fix reading columns asynchronously via JsonNet plugin (#6109) Fixes #6108 --- src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs b/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs index 5d75568f98..10126d25f9 100644 --- a/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs +++ b/src/Npgsql.Json.NET/Internal/JsonNetJsonConverter.cs @@ -51,7 +51,7 @@ static class JsonNetJsonConverter using var stream = reader.GetStream(); var mem = new MemoryStream(); if (async) - await stream.CopyToAsync(mem, Math.Min((int)mem.Length, 81920), cancellationToken).ConfigureAwait(false); + await stream.CopyToAsync(mem, Math.Min((int)stream.Length, 81920), cancellationToken).ConfigureAwait(false); else stream.CopyTo(mem); mem.Position = 0; From 892774fda1c92b53064d775b59e49a8b204d2304 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sun, 1 Jun 2025 12:49:53 +0200 Subject: [PATCH 037/155] Fixes #6107 missed should buffer in biginteger numeric converter (#6117) --- .../Internal/Converters/Primitive/NumericConverters.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs index 00788f99c8..c14a00b608 100644 --- a/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs +++ b/src/Npgsql/Internal/Converters/Primitive/NumericConverters.cs @@ -13,6 +13,9 @@ sealed class BigIntegerNumericConverter : PgStreamingConverter public override BigInteger Read(PgReader reader) { + if (reader.ShouldBuffer(sizeof(short))) + reader.Buffer(sizeof(short)); + var digitCount = reader.ReadInt16(); short[]? digitsFromPool = null; var digits = (digitCount <= StackAllocByteThreshold / sizeof(short) @@ -37,7 +40,9 @@ public override ValueTask ReadAsync(PgReader reader, CancellationTok static async ValueTask AsyncCore(PgReader reader, CancellationToken cancellationToken) { - await reader.BufferAsync(PgNumeric.GetByteCount(0), cancellationToken).ConfigureAwait(false); + if (reader.ShouldBuffer(sizeof(short))) + await reader.BufferAsync(sizeof(short), cancellationToken).ConfigureAwait(false); + var digitCount = reader.ReadInt16(); var digits = new ArraySegment(ArrayPool.Shared.Rent(digitCount), 0, digitCount); var value = ConvertTo(await NumericConverter.ReadAsync(reader, digits, cancellationToken).ConfigureAwait(false)); @@ -132,7 +137,7 @@ static T ConvertTo(in PgNumeric.Builder numeric) static class NumericConverter { - public static int DecimalBasedMaxByteCount = PgNumeric.GetByteCount(PgNumeric.Builder.MaxDecimalNumericDigits); + public static readonly int DecimalBasedMaxByteCount = PgNumeric.GetByteCount(PgNumeric.Builder.MaxDecimalNumericDigits); public static PgNumeric.Builder Read(PgReader reader, Span digits) { From fe7f7755ed78b5292d38c890b110b76ca94e111f Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 3 Jun 2025 11:21:58 +0300 Subject: [PATCH 038/155] Fix logging parameters with batches (#6079) Fixes #6078 --- src/Npgsql/LogMessages.cs | 4 ++-- src/Npgsql/NpgsqlCommand.cs | 5 ++-- src/Npgsql/Util/LoggingEnumerable.cs | 36 ++++++++++++++++++++++++++++ test/Npgsql.Tests/LoggingTests.cs | 22 ++++++++--------- 4 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 src/Npgsql/Util/LoggingEnumerable.cs diff --git a/src/Npgsql/LogMessages.cs b/src/Npgsql/LogMessages.cs index 8d5f471c27..349b91b4b5 100644 --- a/src/Npgsql/LogMessages.cs +++ b/src/Npgsql/LogMessages.cs @@ -180,7 +180,7 @@ static partial class LogMessages Level = LogLevel.Debug, Message = "Executing batch: {BatchCommands}", SkipEnabledCheck = true)] - internal static partial void ExecutingBatchWithParameters(ILogger logger, (string CommandText, object[] Parameters)[] BatchCommands, int ConnectorId); + internal static partial void ExecutingBatchWithParameters(ILogger logger, (string CommandText, IEnumerable Parameters)[] BatchCommands, int ConnectorId); [LoggerMessage( EventId = NpgsqlEventId.CommandExecutionCompleted, @@ -209,7 +209,7 @@ static partial class LogMessages Message = "Batch execution completed (duration={DurationMs}ms): {BatchCommands}", SkipEnabledCheck = true)] internal static partial void BatchExecutionCompletedWithParameters( - ILogger logger, (string CommandText, object[] Parameters)[] BatchCommands, long DurationMs, int ConnectorId); + ILogger logger, (string CommandText, IEnumerable Parameters)[] BatchCommands, long DurationMs, int ConnectorId); [LoggerMessage( EventId = NpgsqlEventId.CancellingCommand, diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 71a646e262..e9d86bb222 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1823,6 +1823,7 @@ internal void LogExecutingCompleted(NpgsqlConnector connector, bool executing) { var logParameters = connector.LoggingConfiguration.IsParameterLoggingEnabled || connector.Settings.LogParameters; var logger = connector.LoggingConfiguration.CommandLogger; + Debug.Assert(executing ? logger.IsEnabled(LogLevel.Debug) : logger.IsEnabled(LogLevel.Information)); if (InternalBatchCommands.Count == 1) { @@ -1860,9 +1861,9 @@ internal void LogExecutingCompleted(NpgsqlConnector connector, bool executing) { if (logParameters) { - var commands = new (string, object[])[InternalBatchCommands.Count]; + var commands = new (string, IEnumerable)[InternalBatchCommands.Count]; for (var i = 0; i < InternalBatchCommands.Count; i++) - commands[i] = (InternalBatchCommands[i].FinalCommandText!, GetParametersForLogging(InternalBatchCommands[i])); + commands[i] = (InternalBatchCommands[i].FinalCommandText!, new LoggingEnumerable(GetParametersForLogging(InternalBatchCommands[i]))); if (executing) LogMessages.ExecutingBatchWithParameters(logger, commands, connector.Id); diff --git a/src/Npgsql/Util/LoggingEnumerable.cs b/src/Npgsql/Util/LoggingEnumerable.cs new file mode 100644 index 0000000000..eabc7ebdd5 --- /dev/null +++ b/src/Npgsql/Util/LoggingEnumerable.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Npgsql.Util; + +// For logging batches we have to use a wrapper for parameters, otherwise they're logged as object[]. See https://github.com/npgsql/npgsql/issues/6078. +sealed class LoggingEnumerable(IEnumerable wrappedEnumerable) : IEnumerable +{ + public IEnumerator GetEnumerator() => wrappedEnumerable.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)wrappedEnumerable).GetEnumerator(); + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append('['); + + var appended = false; + + foreach (var o in wrappedEnumerable) + { + if (appended) + sb.Append(", "); + else + appended = true; + + sb.Append(o); + } + + sb.Append(']'); + + return sb.ToString(); + } +} diff --git a/test/Npgsql.Tests/LoggingTests.cs b/test/Npgsql.Tests/LoggingTests.cs index b9a566b6a8..76f13ab03c 100644 --- a/test/Npgsql.Tests/LoggingTests.cs +++ b/test/Npgsql.Tests/LoggingTests.cs @@ -143,8 +143,8 @@ public async Task Command_ExecuteScalar_multiple_statement_without_parameters() } var executingCommandEvent = listLoggerProvider.Log.Single(l => l.Id == NpgsqlEventId.CommandExecutionCompleted); - Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT 1, System.Object[]), (SELECT 2, System.Object[])]")); - var batchCommands = (IList<(string CommandText, object[] Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); + Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT 1, []), (SELECT 2, [])]")); + var batchCommands = (IList<(string CommandText, IEnumerable Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); Assert.That(batchCommands.Count, Is.EqualTo(2)); Assert.That(batchCommands[0].CommandText, Is.EqualTo("SELECT 1")); Assert.That(batchCommands[0].Parameters, Is.Empty); @@ -171,13 +171,13 @@ public async Task Command_ExecuteScalar_multiple_statement_with_parameters() } var executingCommandEvent = listLoggerProvider.Log.Single(l => l.Id == NpgsqlEventId.CommandExecutionCompleted); - Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT $1, System.Object[]), (SELECT $1, System.Object[])]")); - var batchCommands = (IList<(string CommandText, object[] Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); + Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT $1, [8]), (SELECT $1, [9])]")); + var batchCommands = (IList<(string CommandText, IEnumerable Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); Assert.That(batchCommands.Count, Is.EqualTo(2)); Assert.That(batchCommands[0].CommandText, Is.EqualTo("SELECT $1")); - Assert.That(batchCommands[0].Parameters[0], Is.EqualTo(8)); + Assert.That(batchCommands[0].Parameters.First(), Is.EqualTo(8)); Assert.That(batchCommands[1].CommandText, Is.EqualTo("SELECT $1")); - Assert.That(batchCommands[1].Parameters[0], Is.EqualTo(9)); + Assert.That(batchCommands[1].Parameters.First(), Is.EqualTo(9)); AssertLoggingStateDoesNotContain(executingCommandEvent, "Parameters"); if (!IsMultiplexing) @@ -256,21 +256,19 @@ public async Task Batch_ExecuteScalar_multiple_statements_with_parameters() var executingCommandEvent = listLoggerProvider.Log.Single(l => l.Id == NpgsqlEventId.CommandExecutionCompleted); - // Note: the message formatter of Microsoft.Extensions.Logging doesn't seem to handle arrays inside tuples, so we get the - // following ugliness (https://github.com/dotnet/runtime/issues/63165). Serilog handles this fine. - Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT $1, System.Object[]), (SELECT $1, 9, System.Object[])]")); + Assert.That(executingCommandEvent.Message, Does.Contain("Batch execution completed").And.Contains("[(SELECT $1, [8]), (SELECT $1, 9, [9])]")); AssertLoggingStateDoesNotContain(executingCommandEvent, "CommandText"); AssertLoggingStateDoesNotContain(executingCommandEvent, "Parameters"); if (!IsMultiplexing) AssertLoggingStateContains(executingCommandEvent, "ConnectorId", conn.ProcessID); - var batchCommands = (IList<(string CommandText, object[] Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); + var batchCommands = (IList<(string CommandText, IEnumerable Parameters)>)AssertLoggingStateContains(executingCommandEvent, "BatchCommands"); Assert.That(batchCommands.Count, Is.EqualTo(2)); Assert.That(batchCommands[0].CommandText, Is.EqualTo("SELECT $1")); - Assert.That(batchCommands[0].Parameters[0], Is.EqualTo(8)); + Assert.That(batchCommands[0].Parameters.First(), Is.EqualTo(8)); Assert.That(batchCommands[1].CommandText, Is.EqualTo("SELECT $1, 9")); - Assert.That(batchCommands[1].Parameters[0], Is.EqualTo(9)); + Assert.That(batchCommands[1].Parameters.First(), Is.EqualTo(9)); } [Test] From d9c1b0826df58f58bcf41a921bc2b2109904e8a2 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 16 Jun 2025 17:18:26 +0300 Subject: [PATCH 039/155] Implement GSSAPI session encryption (#6131) Closes #2957 --- .../Internal/IntegratedSecurityHandler.cs | 12 +- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 22 +- .../NpgsqlConnector.FrontendMessages.cs | 13 + src/Npgsql/Internal/NpgsqlConnector.cs | 251 +++++++++++++++++- src/Npgsql/NpgsqlConnection.cs | 7 +- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 38 +++ src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 2 +- src/Npgsql/PostgresEnvironment.cs | 2 + src/Npgsql/PublicAPI.Unshipped.txt | 6 + src/Npgsql/Util/GSSStream.cs | 177 ++++++++++++ test/Npgsql.Tests/SecurityTests.cs | 14 +- test/Npgsql.Tests/Support/PgPostmasterMock.cs | 12 + 12 files changed, 522 insertions(+), 34 deletions(-) create mode 100644 src/Npgsql/Util/GSSStream.cs diff --git a/src/Npgsql/Internal/IntegratedSecurityHandler.cs b/src/Npgsql/Internal/IntegratedSecurityHandler.cs index 2b2f2f1bb9..5edb826497 100644 --- a/src/Npgsql/Internal/IntegratedSecurityHandler.cs +++ b/src/Npgsql/Internal/IntegratedSecurityHandler.cs @@ -16,7 +16,10 @@ class IntegratedSecurityHandler return new(); } - public virtual ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector) + public virtual ValueTask NegotiateAuthentication(bool async, 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) => throw new NotSupportedException(string.Format(NpgsqlStrings.IntegratedSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableIntegratedSecurity))); } @@ -27,6 +30,9 @@ 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) - => new(connector.AuthenticateGSS(async)); + public override ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector, CancellationToken cancellationToken) + => connector.AuthenticateGSS(async, 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 8dc5231fd2..4d5fccbad5 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -33,7 +33,13 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async, Canc case AuthenticationRequestType.Ok: // If we didn't complete authentication, check whether it's allowed if (!authenticated) + { + // User requested GSS authentication, but server said that no auth is required + // If and only if our connection is gss encrypted, we consider us already authenticated + if (requiredAuthModes.HasFlag(RequireAuthMode.GSS) && IsGssEncrypted) + return; ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.None); + } return; case AuthenticationRequestType.CleartextPassword: @@ -55,7 +61,7 @@ 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).ConfigureAwait(false); + await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, this, cancellationToken).ConfigureAwait(false); return; case AuthenticationRequestType.GSSContinue: @@ -207,7 +213,7 @@ internal void AuthenticateSASLSha256Plus(ref string mechanism, ref string cbindF // try authenticate without channel binding even though both // the client and server supported it. The SCRAM exchange // checks for that, to prevent downgrade attacks. - if (!IsSecure) + if (!IsSslEncrypted) throw new NpgsqlException("Server offered SCRAM-SHA-256-PLUS authentication over a non-SSL connection"); var sslStream = (SslStream)_stream; @@ -321,7 +327,7 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio await Flush(async, cancellationToken).ConfigureAwait(false); } - internal async Task AuthenticateGSS(bool async) + internal async ValueTask AuthenticateGSS(bool async, CancellationToken cancellationToken) { var targetName = $"{KerberosServiceName}/{Host}"; @@ -331,8 +337,8 @@ internal async Task AuthenticateGSS(bool async) using var authContext = new NegotiateAuthentication(clientOptions); var data = authContext.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; Debug.Assert(statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded); - await WritePassword(data, 0, data.Length, async, UserCancellationToken).ConfigureAwait(false); - await Flush(async, UserCancellationToken).ConfigureAwait(false); + await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false); + await Flush(async, cancellationToken).ConfigureAwait(false); while (true) { var response = ExpectAny(await ReadMessage(async).ConfigureAwait(false), this); @@ -340,15 +346,15 @@ internal async Task AuthenticateGSS(bool async) break; if (response is not AuthenticationGSSContinueMessage gssMsg) throw new NpgsqlException($"Received unexpected authentication request message {response.AuthRequestType}"); - data = authContext.GetOutgoingBlob(gssMsg.AuthenticationData.AsSpan(), out statusCode)!; + data = authContext.GetOutgoingBlob(gssMsg.AuthenticationData.AsSpan(), out statusCode); if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) throw new NpgsqlException($"Error while authenticating GSS/SSPI: {statusCode}"); // We might get NegotiateAuthenticationStatusCode.Completed but the data will not be null // This can happen if it's the first cycle, in which case we have to send that data to complete handshake (#4888) if (data is null) continue; - await WritePassword(data, 0, data.Length, async, UserCancellationToken).ConfigureAwait(false); - await Flush(async, UserCancellationToken).ConfigureAwait(false); + await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false); + await Flush(async, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs b/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs index 9e0fd45dd3..b801b11b84 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs @@ -396,6 +396,19 @@ internal void WriteSslRequest() WriteBuffer.WriteInt32(80877103); } + internal void WriteGSSEncryptRequest() + { + const int len = sizeof(int) + // Length + sizeof(int); // GSSEnc request code + + WriteBuffer.StartMessage(len); + if (WriteBuffer.WriteSpaceLeft < len) + Flush(false).GetAwaiter().GetResult(); + + WriteBuffer.WriteInt32(len); + WriteBuffer.WriteInt32(80877104); + } + internal void WriteStartup(Dictionary parameters) { const int protocolVersion3 = 3 << 16; // 196608 diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 6a3b71d576..c8916acd0a 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.Data; using System.Diagnostics; @@ -19,10 +20,11 @@ using System.Threading.Tasks; using Npgsql.BackendMessages; using Npgsql.Util; -using static Npgsql.Util.Statics; using Microsoft.Extensions.Logging; using Npgsql.Properties; +using static Npgsql.Util.Statics; + namespace Npgsql.Internal; /// @@ -493,7 +495,9 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca activity = NpgsqlActivitySource.ConnectionOpen(this); - await OpenCore(this, username, Settings.SslMode, timeout, async, cancellationToken).ConfigureAwait(false); + var gssEncMode = GetGssEncMode(Settings); + + await OpenCore(this, username, Settings.SslMode, gssEncMode, timeout, async, cancellationToken).ConfigureAwait(false); if (activity is not null) NpgsqlActivitySource.Enrich(activity, this); @@ -583,12 +587,12 @@ static async Task OpenCore( NpgsqlConnector conn, string username, SslMode sslMode, + GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { - await conn.RawOpen(sslMode, timeout, async, cancellationToken).ConfigureAwait(false); - + await conn.RawOpen(sslMode, gssEncMode, timeout, async, cancellationToken).ConfigureAwait(false); timeout.CheckAndApply(conn); conn.WriteStartupMessage(username); await conn.Flush(async, cancellationToken).ConfigureAwait(false); @@ -598,10 +602,21 @@ 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 (Exception e) when + // Any error after trying with GSS encryption + (gssEncMode == GssEncryptionMode.Prefer || + // Auth error with/without SSL + (e is PostgresException { SqlState: PostgresErrorCodes.InvalidAuthorizationSpecification } && + (sslMode == SslMode.Prefer && conn.IsSslEncrypted || sslMode == SslMode.Allow && !conn.IsSslEncrypted))) { + if (gssEncMode == GssEncryptionMode.Prefer) + { + conn.ConnectionLogger.LogTrace(e, "Error while opening physical connection with GSS encryption, retrying without it"); + gssEncMode = GssEncryptionMode.Disable; + } + else + sslMode = sslMode == SslMode.Prefer ? SslMode.Disable : SslMode.Require; + cancellationRegistration.Dispose(); Debug.Assert(!conn.IsBroken); @@ -612,7 +627,8 @@ static async Task OpenCore( await OpenCore( conn, username, - sslMode == SslMode.Prefer ? SslMode.Disable : SslMode.Require, + sslMode, + gssEncMode, timeout, async, cancellationToken).ConfigureAwait(false); @@ -638,6 +654,131 @@ await OpenCore( } } + internal async ValueTask GSSEncrypt(bool async, bool isRequired, CancellationToken cancellationToken) + { + ConnectionLogger.LogTrace("Negotiating GSS encryption"); + + var targetName = $"{KerberosServiceName}/{Host}"; + var clientOptions = new NegotiateAuthenticationClientOptions { TargetName = targetName }; + + NegotiateOptionsCallback?.Invoke(clientOptions); + + var authentication = new NegotiateAuthentication(clientOptions); + + try + { + var data = authentication.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; + if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) + { + // Unable to retrieve credentials + // If it's required, throw an appropriate exception + if (isRequired) + throw new NpgsqlException($"Unable to negotiate GSS encryption: {statusCode}"); + + return GssEncryptionResult.GetCredentialFailure; + } + + WriteGSSEncryptRequest(); + await Flush(async, cancellationToken).ConfigureAwait(false); + + await ReadBuffer.Ensure(1, async).ConfigureAwait(false); + var response = (char)ReadBuffer.ReadByte(); + + // TODO: Server can respond with an error here + // but according to documentation we shouldn't display this error to the user/application + // since the server has not been authenticated (CVE-2024-10977) + // See https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-GSSAPI + switch (response) + { + default: + throw new NpgsqlException($"Received unknown response {response} for GSSEncRequest (expecting G or N)"); + case 'N': + return GssEncryptionResult.NegotiateFailure; + case 'G': + break; + } + + if (ReadBuffer.ReadBytesLeft > 0) + throw new NpgsqlException( + "Additional unencrypted data received after GSS encryption negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); + + var lengthBuffer = new byte[4]; + + await WriteGssEncryptMessage(async, data, lengthBuffer).ConfigureAwait(false); + + while (true) + { + if (async) + await _stream.ReadExactlyAsync(lengthBuffer, cancellationToken).ConfigureAwait(false); + else + _stream.ReadExactly(lengthBuffer); + + var messageLength = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned(ref lengthBuffer[0])) + : Unsafe.ReadUnaligned(ref lengthBuffer[0]); + + var buffer = ArrayPool.Shared.Rent(messageLength); + if (async) + await _stream.ReadExactlyAsync(buffer.AsMemory(0, messageLength), cancellationToken).ConfigureAwait(false); + else + _stream.ReadExactly(buffer.AsSpan(0, messageLength)); + + data = authentication.GetOutgoingBlob(buffer.AsSpan(0, messageLength), out statusCode); + ArrayPool.Shared.Return(buffer, clearArray: true); + if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) + throw new NpgsqlException($"Error while negotiating GSS encryption: {statusCode}"); + + // TODO: the code below is the copy from GSS/SSPI auth + // It's unknown whether it holds true here or not + + // We might get NegotiateAuthenticationStatusCode.Completed but the data will not be null + // This can happen if it's the first cycle, in which case we have to send that data to complete handshake (#4888) + if (data is null) + { + Debug.Assert(statusCode == NegotiateAuthenticationStatusCode.Completed); + break; + } + + await WriteGssEncryptMessage(async, data, lengthBuffer).ConfigureAwait(false); + } + + _stream = new GSSStream(_stream, authentication); + ReadBuffer.Underlying = _stream; + WriteBuffer.Underlying = _stream; + IsGssEncrypted = true; + authentication = null; + + ConnectionLogger.LogTrace("GSS encryption successful"); + return GssEncryptionResult.Success; + + async ValueTask WriteGssEncryptMessage(bool async, byte[] data, byte[] lengthBuffer) + { + BinaryPrimitives.WriteInt32BigEndian(lengthBuffer, data.Length); + + if (async) + { + await _stream.WriteAsync(lengthBuffer, cancellationToken).ConfigureAwait(false); + await _stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _stream.Write(lengthBuffer); + _stream.Write(data); + _stream.Flush(); + } + } + } + catch (Exception e) + { + throw new NpgsqlException("Exception while performing GSS encryption", e); + } + finally + { + authentication?.Dispose(); + } + } + internal async ValueTask QueryDatabaseState( NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken = default) { @@ -762,7 +903,7 @@ async ValueTask GetUsernameAsyncInternal() } } - async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) + async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { try { @@ -792,13 +933,31 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat timeout.CheckAndApply(this); - IsSecure = false; + IsSslEncrypted = false; + IsGssEncrypted = false; + + var gssEncryptResult = await TryNegotiateGssEncryption(gssEncryptionMode, async, cancellationToken).ConfigureAwait(false); + if (gssEncryptResult == GssEncryptionResult.Success) + return; + + timeout.CheckAndApply(this); if (GetSslNegotiation(Settings) == SslNegotiation.Direct) { // We already check that in NpgsqlConnectionStringBuilder.PostProcessAndValidate, but since we also allow environment variables... if (Settings.SslMode is not SslMode.Require and not SslMode.VerifyCA and not SslMode.VerifyFull) throw new ArgumentException("SSL Mode has to be Require or higher to be used with direct SSL Negotiation"); + if (gssEncryptResult == GssEncryptionResult.NegotiateFailure) + { + // We can be here only if it's fallback from preferred (but failed) gss encryption + // In this case, direct encryption isn't going to work anymore, so we throw a bogus exception to retry again without gss + // Alternatively, we can instead just go with the usual route of writing SslRequest, ignoring direct ssl + // But this is how libpq works + Debug.Assert(gssEncryptionMode == GssEncryptionMode.Prefer); + // The exception message doesn't matter since we're going to retry again + throw new NpgsqlException(); + } + await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false); if (ReadBuffer.ReadBytesLeft > 0) throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); @@ -845,6 +1004,25 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat } } + async ValueTask TryNegotiateGssEncryption(GssEncryptionMode gssEncryptionMode, bool async, CancellationToken cancellationToken) + { + // GetCredentialFailure is essentially a nop (since we didn't send anything over the wire) + // So we can proceed further as if gss encryption wasn't even attempted + if (gssEncryptionMode == GssEncryptionMode.Disable) return GssEncryptionResult.GetCredentialFailure; + + if (ConnectedEndPoint!.AddressFamily == AddressFamily.Unix) + { + if (gssEncryptionMode == GssEncryptionMode.Prefer) + return GssEncryptionResult.GetCredentialFailure; + + Debug.Assert(gssEncryptionMode == GssEncryptionMode.Require); + throw new NpgsqlException("GSS encryption isn't supported over unix socket"); + } + + return await DataSource.IntegratedSecurityHandler.GSSEncrypt(async, gssEncryptionMode == GssEncryptionMode.Require, this, cancellationToken) + .ConfigureAwait(false); + } + static SslNegotiation GetSslNegotiation(NpgsqlConnectionStringBuilder settings) { if (settings.UserProvidedSslNegotiation is { } userProvidedSslNegotiation) @@ -859,8 +1037,24 @@ static SslNegotiation GetSslNegotiation(NpgsqlConnectionStringBuilder settings) return SslNegotiation.Postgres; } + static GssEncryptionMode GetGssEncMode(NpgsqlConnectionStringBuilder settings) + { + if (settings.UserProvidedGssEncMode is { } userProvidedGssEncMode) + return userProvidedGssEncMode; + + if (PostgresEnvironment.GssEncryptionMode is { } gssEncModeEnv) + { + if (Enum.TryParse(gssEncModeEnv, ignoreCase: true, out var gssEncMode)) + return gssEncMode; + } + + return GssEncryptionMode.Disable; + } + internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { + ConnectionLogger.LogTrace("Negotiating SSL encryption"); + var clientCertificates = new X509Certificate2Collection(); var certPath = Settings.SslCertificate ?? PostgresEnvironment.SslCert ?? PostgresEnvironment.SslCertDefault; @@ -981,7 +1175,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, ReadBuffer.Underlying = _stream; WriteBuffer.Underlying = _stream; - IsSecure = true; + IsSslEncrypted = true; ConnectionLogger.LogTrace("SSL negotiation successful"); } catch @@ -1680,7 +1874,12 @@ internal void ClearTransaction(Exception? disposeReason = null) /// /// Returns whether SSL is being used for the connection /// - internal bool IsSecure { get; private set; } + internal bool IsSslEncrypted { get; private set; } + + /// + /// Returns whether GSS is being used for the connection + /// + internal bool IsGssEncrypted { get; private set; } /// /// Returns whether SCRAM-SHA256 is being used for the connection @@ -1898,11 +2097,28 @@ internal bool PerformPostgresCancellation() void DoCancelRequest(int backendProcessId, int backendSecretKey) { Debug.Assert(State == ConnectorState.Closed); + var gssEncMode = GetGssEncMode(Settings); try { - RawOpen(Settings.SslMode, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, CancellationToken.None) - .GetAwaiter().GetResult(); + try + { + RawOpen(Settings.SslMode, gssEncMode, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, + CancellationToken.None) + .GetAwaiter().GetResult(); + } + catch (Exception e) when (gssEncMode == GssEncryptionMode.Prefer) + { + ConnectionLogger.LogTrace(e, "Error while opening physical connection with GSS encryption, retrying without it"); + Cleanup(); + + // If we hit an error with gss encryption + // Retry again without it + RawOpen(Settings.SslMode, GssEncryptionMode.Disable, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, + CancellationToken.None) + .GetAwaiter().GetResult(); + } + WriteCancelRequest(backendProcessId, backendSecretKey); Flush(); @@ -2975,4 +3191,11 @@ enum DataRowLoadingMode Skip } +enum GssEncryptionResult +{ + GetCredentialFailure, + NegotiateFailure, + Success +} + #endregion diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index 4b989349f7..c63f5bc4b6 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -999,7 +999,12 @@ internal void OnNotification(NpgsqlNotificationEventArgs e) /// /// Returns whether SSL is being used for the connection. /// - internal bool IsSecure => CheckOpenAndRunInTemporaryScope(c => c.IsSecure); + internal bool IsSslEncrypted => CheckOpenAndRunInTemporaryScope(c => c.IsSslEncrypted); + + /// + /// Returns whether GSS encryption is being used for the connection. + /// + internal bool IsGssEncrypted => CheckOpenAndRunInTemporaryScope(c => c.IsGssEncrypted); /// /// Returns whether SCRAM-SHA256 is being user for the connection diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index f0b258d356..35a6ed04e0 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -479,6 +479,25 @@ public SslNegotiation SslNegotiation internal SslNegotiation? UserProvidedSslNegotiation { get; private set; } + /// + /// Controls whether GSS encryption is required, disabled or preferred, depending on server support. + /// + [Category("Security")] + [Description("Controls whether GSS encryption is required, disabled or preferred, depending on server support.")] + [DisplayName("GSS Encryption Mode")] + [NpgsqlConnectionStringProperty] + public GssEncryptionMode GssEncryptionMode + { + get => UserProvidedGssEncMode ?? GssEncryptionMode.Disable; + set + { + UserProvidedGssEncMode = value; + SetValue(nameof(GssEncryptionMode), value); + } + } + + internal GssEncryptionMode? UserProvidedGssEncMode { get; private set; } + /// /// Location of a client certificate to be sent to the server. /// @@ -1725,6 +1744,25 @@ public enum SslNegotiation Direct } +/// +/// Specifies how to manage GSS encryption. +/// +public enum GssEncryptionMode +{ + /// + /// GSS encryption is disabled. If the server requires GSS encryption, the connection will fail. + /// + Disable, + /// + /// Prefer GSS encrypted connections if the server allows them, but allow connections without GSS encryption. + /// + Prefer, + /// + /// Fail the connection if the server doesn't support GSS encryption. + /// + Require +} + /// /// Specifies how to manage channel binding. /// diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 3c7de21fb7..3fa0a5a9fd 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -610,7 +610,7 @@ public NpgsqlSlimDataSourceBuilder EnableTransportSecurity() } /// - /// Enables the possibility to use GSS/SSPI authentication for connections to PostgreSQL. This does not guarantee that it will + /// Enables the possibility to use GSS/SSPI authentication and encryption for connections to PostgreSQL. This does not guarantee that it will /// actually be used; see for more details. /// /// The same builder instance so that multiple calls can be chained. diff --git a/src/Npgsql/PostgresEnvironment.cs b/src/Npgsql/PostgresEnvironment.cs index 389df7d085..558f6cfe9f 100644 --- a/src/Npgsql/PostgresEnvironment.cs +++ b/src/Npgsql/PostgresEnvironment.cs @@ -50,6 +50,8 @@ internal static string? SslCertRootDefault internal static string? SslNegotiation => Environment.GetEnvironmentVariable("PGSSLNEGOTIATION"); + internal static string? GssEncryptionMode => Environment.GetEnvironmentVariable("PGGSSENCMODE"); + internal static string? RequireAuth => Environment.GetEnvironmentVariable("PGREQUIREAUTH"); static string? GetHomeDir() diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 47198a4165..aad4e3e227 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -1,8 +1,14 @@ #nullable enable abstract Npgsql.NpgsqlDataSource.Clear() -> void +Npgsql.GssEncryptionMode +Npgsql.GssEncryptionMode.Disable = 0 -> Npgsql.GssEncryptionMode +Npgsql.GssEncryptionMode.Prefer = 1 -> Npgsql.GssEncryptionMode +Npgsql.GssEncryptionMode.Require = 2 -> Npgsql.GssEncryptionMode Npgsql.NpgsqlConnection.CloneWithAsync(string! connectionString, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Action? Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void +Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.get -> Npgsql.GssEncryptionMode +Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.set -> void Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string? Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation diff --git a/src/Npgsql/Util/GSSStream.cs b/src/Npgsql/Util/GSSStream.cs new file mode 100644 index 0000000000..c6c47bd4ca --- /dev/null +++ b/src/Npgsql/Util/GSSStream.cs @@ -0,0 +1,177 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Net.Security; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Npgsql.Util; + +// For more detailed explanation of communication protocol +// See https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-GSSAPI +sealed class GSSStream : Stream +{ + // At most, postgres supports GSS messages up to 16kb + // We use the recommended value of 8kb for the write buffer + // Which will result in messages of slightly larger than 8kb + const int MaxWriteMessageSizeLimit = 8 * 1024; + const int MaxReadMessageSizeLimit = 16 * 1024; + + readonly Stream _stream; + readonly NegotiateAuthentication _authentication; + + readonly ArrayBufferWriter _writeBuffer; + readonly byte[] _writeLengthBuffer; + + readonly byte[] _readBuffer; + int _readPosition; + int _leftToRead; + + internal GSSStream(Stream stream, NegotiateAuthentication authentication) + { + _stream = stream; + _authentication = authentication; + // While we guarantee that unencrypted messages are at most 8kb + // Encrypting them will result in messages slightly larger than the original size + // Which is why the initial capacity has an additional 2kb of free space + _writeBuffer = new ArrayBufferWriter(MaxWriteMessageSizeLimit + 2048); + _writeLengthBuffer = new byte[4]; + _readBuffer = new byte[MaxReadMessageSizeLimit]; + } + + public override void Write(ReadOnlySpan buffer) + { + var start = 0; + while (start != buffer.Length) + { + var lengthToWrite = Math.Min(buffer.Length - start, MaxWriteMessageSizeLimit); + var result = _authentication.Wrap( + buffer.Slice(start, lengthToWrite), + _writeBuffer, + _authentication.IsEncrypted, + out _); + if (result != NegotiateAuthenticationStatusCode.Completed) + throw new NpgsqlException($"Error while encrypting buffer: {result}"); + + var written = _writeBuffer.WrittenMemory; + Unsafe.WriteUnaligned(ref _writeLengthBuffer[0], BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(written.Length) : written.Length); + + _stream.Write(_writeLengthBuffer); + _stream.Write(buffer.Slice(start, lengthToWrite)); + + _writeBuffer.ResetWrittenCount(); + start += lengthToWrite; + } + } + + public override void Write(byte[] buffer, int offset, int count) + => Write(buffer.AsSpan(offset, count)); + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + var start = 0; + while (start != buffer.Length) + { + var lengthToWrite = Math.Min(buffer.Length - start, MaxWriteMessageSizeLimit); + var result = _authentication.Wrap( + buffer.Slice(start, lengthToWrite).Span, + _writeBuffer, + _authentication.IsEncrypted, + out _); + if (result != NegotiateAuthenticationStatusCode.Completed) + throw new NpgsqlException($"Error while encrypting buffer: {result}"); + + var written = _writeBuffer.WrittenMemory; + Unsafe.WriteUnaligned(ref _writeLengthBuffer[0], BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(written.Length) : written.Length); + + await _stream.WriteAsync(_writeLengthBuffer, cancellationToken).ConfigureAwait(false); + await _stream.WriteAsync(_writeBuffer.WrittenMemory, cancellationToken).ConfigureAwait(false); + + _writeBuffer.ResetWrittenCount(); + start += lengthToWrite; + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + + public override void Flush() => _stream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => _stream.FlushAsync(cancellationToken); + + public override int Read(Span buffer) + { + if (_leftToRead == 0) + { + _stream.ReadExactly(_readBuffer.AsSpan(0, 4)); + var messageLength = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned(ref _readBuffer[0])) + : Unsafe.ReadUnaligned(ref _readBuffer[0]); + var messageBuffer = _readBuffer.AsSpan(0, messageLength); + _stream.ReadExactly(messageBuffer); + var result = _authentication.UnwrapInPlace(messageBuffer, out _readPosition, out _leftToRead, out _); + if (result != NegotiateAuthenticationStatusCode.Completed) + throw new NpgsqlException($"Error while decrypting buffer: {result}"); + } + + var maxRead = Math.Min(_leftToRead, buffer.Length); + _readBuffer.AsSpan(_readPosition, maxRead).CopyTo(buffer); + _readPosition += maxRead; + _leftToRead -= maxRead; + return maxRead; + } + + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_leftToRead == 0) + { + await _stream.ReadExactlyAsync(_readBuffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); + var messageLength = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned(ref _readBuffer[0])) + : Unsafe.ReadUnaligned(ref _readBuffer[0]); + var messageBuffer = _readBuffer.AsMemory(0, messageLength); + await _stream.ReadExactlyAsync(messageBuffer, cancellationToken).ConfigureAwait(false); + var result = _authentication.UnwrapInPlace(messageBuffer.Span, out _readPosition, out _leftToRead, out _); + if (result != NegotiateAuthenticationStatusCode.Completed) + throw new NpgsqlException($"Error while decrypting buffer: {result}"); + } + + var maxRead = Math.Min(_leftToRead, buffer.Length); + _readBuffer.AsMemory(_readPosition, maxRead).CopyTo(buffer); + _readPosition += maxRead; + _leftToRead -= maxRead; + return maxRead; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + + public override void Close() => _stream.Close(); + + protected override void Dispose(bool disposing) + { + _authentication.Dispose(); + _stream.Dispose(); + } + + public override ValueTask DisposeAsync() => _stream.DisposeAsync(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } +} diff --git a/test/Npgsql.Tests/SecurityTests.cs b/test/Npgsql.Tests/SecurityTests.cs index 13b7ca7495..7f47ea8111 100644 --- a/test/Npgsql.Tests/SecurityTests.cs +++ b/test/Npgsql.Tests/SecurityTests.cs @@ -20,7 +20,7 @@ public async Task Basic_ssl() csb.SslMode = SslMode.Require; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.That(conn.IsSecure, Is.True); + Assert.That(conn.IsSslEncrypted, Is.True); } [Test, Description("Default user must run with md5 password encryption")] @@ -72,7 +72,7 @@ public void IsSecure_without_ssl() { using var dataSource = CreateDataSource(csb => csb.SslMode = SslMode.Disable); using var conn = dataSource.OpenConnection(); - Assert.That(conn.IsSecure, Is.False); + Assert.That(conn.IsSslEncrypted, Is.False); } [Test, Explicit("Needs to be set up (and run with with Kerberos credentials on Linux)")] @@ -248,7 +248,7 @@ public async Task Connect_with_only_ssl_allowed_user([Values] bool multiplexing, csb.KeepAlive = keepAlive ? 10 : 0; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSecure); + Assert.IsTrue(conn.IsSslEncrypted); } catch (Exception e) when (!IsOnBuildServer) { @@ -277,7 +277,7 @@ public async Task Connect_with_only_non_ssl_allowed_user([Values] bool multiplex csb.KeepAlive = keepAlive ? 10 : 0; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsFalse(conn.IsSecure); + Assert.IsFalse(conn.IsSslEncrypted); } catch (NpgsqlException ex) when (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && ex.InnerException is IOException) { @@ -400,7 +400,7 @@ public async Task Bug4305_Secure([Values] bool async) try { conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSecure); + Assert.IsTrue(conn.IsSslEncrypted); } catch (Exception e) when (!IsOnBuildServer) { @@ -451,7 +451,7 @@ public async Task Bug4305_not_Secure([Values] bool async) try { conn = await dataSource.OpenConnectionAsync(); - Assert.IsFalse(conn.IsSecure); + Assert.IsFalse(conn.IsSslEncrypted); } catch (Exception e) when (!IsOnBuildServer) { @@ -494,7 +494,7 @@ public async Task Direct_ssl_negotiation() csb.SslNegotiation = SslNegotiation.Direct; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSecure); + Assert.IsTrue(conn.IsSslEncrypted); } [Test] diff --git a/test/Npgsql.Tests/Support/PgPostmasterMock.cs b/test/Npgsql.Tests/Support/PgPostmasterMock.cs index 3a59ccc2f9..178de2d01d 100644 --- a/test/Npgsql.Tests/Support/PgPostmasterMock.cs +++ b/test/Npgsql.Tests/Support/PgPostmasterMock.cs @@ -16,6 +16,7 @@ class PgPostmasterMock : IAsyncDisposable const int WriteBufferSize = 8192; const int CancelRequestCode = 1234 << 16 | 5678; const int SslRequest = 80877103; + const int GssRequest = 80877104; static readonly Encoding Encoding = NpgsqlWriteBuffer.UTF8Encoding; static readonly Encoding RelaxedEncoding = NpgsqlWriteBuffer.RelaxedUTF8Encoding; @@ -148,6 +149,17 @@ async Task Accept(bool completeCancellationImmediat await readBuffer.EnsureAsync(len - 4); var request = readBuffer.ReadInt32(); + if (request == GssRequest) + { + writeBuffer.WriteByte((byte)'N'); + await writeBuffer.Flush(async: true); + + await readBuffer.EnsureAsync(4); + len = readBuffer.ReadInt32(); + await readBuffer.EnsureAsync(len - 4); + request = readBuffer.ReadInt32(); + } + if (request == SslRequest) { writeBuffer.WriteByte((byte)'N'); From 317c4e357a0c9198ed378476bf57236e1604efeb Mon Sep 17 00:00:00 2001 From: Michael Todorovic Date: Tue, 17 Jun 2025 12:52:36 +0200 Subject: [PATCH 040/155] Add support for PGAPPNAME to set application name (#6139) Signed-off-by: Michael Todorovic --- src/Npgsql/Internal/NpgsqlConnector.cs | 5 ++-- src/Npgsql/PostgresEnvironment.cs | 2 ++ test/Npgsql.Tests/ConnectionTests.cs | 41 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index c8916acd0a..7f720f5882 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -833,8 +833,9 @@ void WriteStartupMessage(string username) if (Settings.Database is not null) startupParams["database"] = Settings.Database; - if (Settings.ApplicationName?.Length > 0) - startupParams["application_name"] = Settings.ApplicationName; + var applicationName = Settings.ApplicationName ?? PostgresEnvironment.AppName; + if (applicationName?.Length > 0) + startupParams["application_name"] = applicationName; if (Settings.SearchPath?.Length > 0) startupParams["search_path"] = Settings.SearchPath; diff --git a/src/Npgsql/PostgresEnvironment.cs b/src/Npgsql/PostgresEnvironment.cs index 558f6cfe9f..3ba874ae4c 100644 --- a/src/Npgsql/PostgresEnvironment.cs +++ b/src/Npgsql/PostgresEnvironment.cs @@ -54,6 +54,8 @@ internal static string? SslCertRootDefault internal static string? RequireAuth => Environment.GetEnvironmentVariable("PGREQUIREAUTH"); + internal static string? AppName => Environment.GetEnvironmentVariable("PGAPPNAME"); + static string? GetHomeDir() => Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "APPDATA" : "HOME"); diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 90dbd4ecf1..ac97daf101 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -391,6 +391,47 @@ public async Task Timezone_connection_param() #endregion Timezone + #region Application Name + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6133")] + [NonParallelizable] // Sets environment variable + public async Task Application_name_env_var() + { + const string testAppName = "MyTestApp"; + + // Note that the pool is unaware of the environment variable, so if a connection is + // returned from the pool it may contain the wrong application name + using var _ = SetEnvironmentVariable("PGAPPNAME", testAppName); + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + Assert.That(conn.PostgresParameters["application_name"], Is.EqualTo(testAppName)); + } + + [Test] + public async Task Application_name_connection_param() + { + const string testAppName = "MyTestApp2"; + + await using var dataSource = CreateDataSource(csb => csb.ApplicationName = testAppName); + await using var conn = await dataSource.OpenConnectionAsync(); + Assert.That(conn.PostgresParameters["application_name"], Is.EqualTo(testAppName)); + } + + [Test] + [NonParallelizable] // Sets environment variable + public async Task Application_name_connection_param_overrides_env_var() + { + const string envAppName = "EnvApp"; + const string connAppName = "ConnApp"; + + using var _ = SetEnvironmentVariable("PGAPPNAME", envAppName); + await using var dataSource = CreateDataSource(csb => csb.ApplicationName = connAppName); + await using var conn = await dataSource.OpenConnectionAsync(); + Assert.That(conn.PostgresParameters["application_name"], Is.EqualTo(connAppName)); + } + + #endregion Application Name + #region ConnectionString - Host [TestCase("127.0.0.1", ExpectedResult = new [] { "127.0.0.1:5432" })] From e3921f213b251c80bc35f2622db9c7dc99626c68 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Fri, 20 Jun 2025 13:19:59 +0300 Subject: [PATCH 041/155] Fix returning null from KerberosUsernameProvider.GetUsername with concurrent calls (#6137) Fixes #6136 --- src/Npgsql/KerberosUsernameProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/KerberosUsernameProvider.cs b/src/Npgsql/KerberosUsernameProvider.cs index 591c43fd98..6963e139f0 100644 --- a/src/Npgsql/KerberosUsernameProvider.cs +++ b/src/Npgsql/KerberosUsernameProvider.cs @@ -11,9 +11,9 @@ namespace Npgsql; /// Launches MIT Kerberos klist and parses out the default principal from it. /// Caches the result. /// -sealed class KerberosUsernameProvider +static class KerberosUsernameProvider { - static bool _performedDetection; + static volatile bool _performedDetection; static string? _principalWithRealm; static string? _principalWithoutRealm; From 1b55ebc74d15ef9edff5ab661062de5cd4625a2c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 23 Jun 2025 13:03:52 +0200 Subject: [PATCH 042/155] Add NpgsqlTsVector.Empty (#6145) Closes #6134 --- src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs | 5 +++++ src/Npgsql/PublicAPI.Unshipped.txt | 1 + test/Npgsql.Tests/TypesTests.cs | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs index 7d63a547fe..4dd1e28b08 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTsVector.cs @@ -11,6 +11,11 @@ namespace NpgsqlTypes; /// public sealed class NpgsqlTsVector : IEnumerable, IEquatable { + /// + /// Represents an empty tsvector. + /// + public static readonly NpgsqlTsVector Empty = new NpgsqlTsVector([], noCheck: true); + readonly List _lexemes; internal NpgsqlTsVector(List lexemes, bool noCheck = false) diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index aad4e3e227..a1d261ead0 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -84,3 +84,4 @@ Npgsql.NpgsqlConnection.ReloadTypesAsync(System.Threading.CancellationToken canc *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! +static readonly NpgsqlTypes.NpgsqlTsVector.Empty -> NpgsqlTypes.NpgsqlTsVector! diff --git a/test/Npgsql.Tests/TypesTests.cs b/test/Npgsql.Tests/TypesTests.cs index 113c08b954..1f6b0e8c55 100644 --- a/test/Npgsql.Tests/TypesTests.cs +++ b/test/Npgsql.Tests/TypesTests.cs @@ -86,6 +86,13 @@ public void TsQuery() } #pragma warning restore CS0618 // {NpgsqlTsVector,NpgsqlTsQuery}.Parse are obsolete + [Test] + public void TsVector_empty() + { + Assert.IsEmpty(NpgsqlTsVector.Empty); + Assert.AreEqual(string.Empty, NpgsqlTsVector.Empty.ToString()); + } + [Test] public void TsQueryEquatibility() { From 3dae12114ddd5e59b618972a8fee43b39c2ef7f1 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 2 Jul 2025 15:16:44 +0300 Subject: [PATCH 043/155] Add assert to NpgsqlCommand.Transaction if it's completed (#6151) Closes #6149 --- src/Npgsql/NpgsqlCommand.cs | 8 +++++++- test/Npgsql.Tests/CommandTests.cs | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index e9d86bb222..d5ad24fdb2 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1635,7 +1635,13 @@ internal virtual async ValueTask ExecuteReader(bool async, Com protected override DbTransaction? DbTransaction { get => _transaction; - set => _transaction = (NpgsqlTransaction?)value; + set + { + var tx = (NpgsqlTransaction?)value; + if (tx is { IsCompleted: true }) + throw new InvalidOperationException("Transaction is already completed"); + _transaction = tx; + } } /// diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs index f159d7d97c..8cc36df10a 100644 --- a/test/Npgsql.Tests/CommandTests.cs +++ b/test/Npgsql.Tests/CommandTests.cs @@ -1639,4 +1639,19 @@ await server Assert.That(connection.PostgresParameters, Contains.Key("SomeKey").WithValue("SomeValue")); } + + [Test] + public async Task Completed_transaction_throws([Values] bool commit) + { + await using var conn = await OpenConnectionAsync(); + await using var tx = await conn.BeginTransactionAsync(); + await using var cmd = conn.CreateCommand(); + + if (commit) + await tx.CommitAsync(); + else + await tx.RollbackAsync(); + + Assert.Throws(() => cmd.Transaction = tx); + } } From 016ae357277cf2cf65905b9a1bac76a4673753e4 Mon Sep 17 00:00:00 2001 From: 0MG-DEN <31481586+0MG-DEN@users.noreply.github.com> Date: Fri, 4 Jul 2025 15:10:29 +0300 Subject: [PATCH 044/155] Compare normalized type names (#6011) Fixes #6010 --- src/Npgsql/Internal/TypeInfoMapping.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/TypeInfoMapping.cs b/src/Npgsql/Internal/TypeInfoMapping.cs index afb5325590..c8439de6ac 100644 --- a/src/Npgsql/Internal/TypeInfoMapping.cs +++ b/src/Npgsql/Internal/TypeInfoMapping.cs @@ -71,7 +71,8 @@ public readonly struct TypeInfoMapping(Type type, string dataTypeName, TypeInfoF public Func? TypeMatchPredicate { get; init; } public bool TypeEquals(Type type) => TypeMatchPredicate?.Invoke(type) ?? Type == type; - public bool DataTypeNameEquals(string dataTypeName) + + private bool DataTypeNameEqualsCore(string dataTypeName) { var span = DataTypeName.AsSpan(); return Postgres.DataTypeName.IsFullyQualified(span) @@ -79,6 +80,18 @@ public bool DataTypeNameEquals(string dataTypeName) : span.Equals(Postgres.DataTypeName.ValidatedName(dataTypeName).UnqualifiedNameSpan, StringComparison.Ordinal); } + internal bool DataTypeNameEquals(DataTypeName dataTypeName) + { + var value = dataTypeName.Value; + return DataTypeNameEqualsCore(value); + } + + public bool DataTypeNameEquals(string dataTypeName) + { + var normalized = Postgres.DataTypeName.NormalizeName(dataTypeName); + return DataTypeNameEqualsCore(normalized); + } + string DebuggerDisplay { get @@ -125,7 +138,7 @@ public TypeInfoMappingCollection(IEnumerable items) { var looseTypeMatch = mapping.TypeMatchPredicate is { } pred ? pred(type) : type is null || mapping.Type == type; var typeMatch = type is not null && looseTypeMatch; - var dataTypeMatch = dataTypeName is not null && mapping.DataTypeNameEquals(dataTypeName.Value.Value); + var dataTypeMatch = dataTypeName is not null && mapping.DataTypeNameEquals(dataTypeName.Value); var matchRequirement = mapping.MatchRequirement; if (dataTypeMatch && typeMatch From be916c2391daab20616f406e1d3540f43053e380 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 7 Jul 2025 12:50:25 +0200 Subject: [PATCH 045/155] Do CI testing for PG18 (beta) (#6155) --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c62460816..f10c34ea60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,11 +49,11 @@ jobs: pg_major: 17 config: Release test_tfm: net8.0 -# - os: ubuntu-24.04 -# pg_major: 17 -# config: Release -# test_tfm: net8.0 -# pg_prerelease: 'PG Prerelease' + - os: ubuntu-24.04 + pg_major: 18 + config: Release + test_tfm: net8.0 + pg_prerelease: 'PG Prerelease' outputs: is_release: ${{ steps.analyze_tag.outputs.is_release }} @@ -86,7 +86,7 @@ jobs: # Automated repository configuration sudo apt install -y postgresql-common - sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y + sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v ${{ matrix.pg_major }} -y sudo apt-get update -qq sudo apt-get install -qq postgresql-${{ matrix.pg_major }} export PGDATA=/etc/postgresql/${{ matrix.pg_major }}/main @@ -102,9 +102,9 @@ jobs: sudo -u postgres psql -c "CREATE USER npgsql_tests_nossl SUPERUSER PASSWORD 'npgsql_tests_nossl'" # To disable PostGIS for prereleases (because it usually isn't available until late), surround with the following: - if [ -z "${{ matrix.pg_prerelease }}" ]; then + #if [ -z "${{ matrix.pg_prerelease }}" ]; then sudo apt-get install -qq postgresql-${{ matrix.pg_major }}-postgis-${{ env.postgis_version }} - fi + #fi if [ ${{ matrix.pg_major }} -ge 14 ]; then sudo sed -i "s|unix_socket_directories = '/var/run/postgresql'|unix_socket_directories = '/var/run/postgresql, @/npgsql_unix'|" $PGDATA/postgresql.conf From c378bdbc141dedd00f4d67b2eaa246c5e8d92666 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Mon, 4 Aug 2025 17:10:00 +0300 Subject: [PATCH 046/155] Fix infinite consume on error with connection break (#6161) Fixes #6160 --- src/Npgsql/NpgsqlDataReader.cs | 8 +++++++- test/Npgsql.Tests/ReaderTests.cs | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 4add2e970d..86963afd4a 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -985,7 +985,13 @@ async Task Consume(bool async, Exception? firstException = null) // Skip over the other result sets. Note that this does tally records affected from CommandComplete messages, and properly sets // state for auto-prepared statements - while (true) + // + // The only exception is when the connector is broken (which can happen in the middle of consuming) + // As then there is no point in going forward + // + // While we can also check our local state (State == Closed) + // It's probably better to rely on connector since it's private and its state can't be changed + while (!Connector.IsBroken) { try { diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index b46342153f..839ec5b610 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -2371,6 +2371,41 @@ await pgMock Assert.That(conn.Connector!.State, Is.EqualTo(ConnectorState.Ready)); } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6160")] + [Description("Consuming result set shouldn't go infinite in case connection is broken")] + public async Task Bug6160() + { + var csb = new NpgsqlConnectionStringBuilder(ConnectionString) + { + // Set to -1 to trigger immediate connection break on timeout + CancellationTimeout = -1, + CommandTimeout = 1 + }; + await using var postmasterMock = PgPostmasterMock.Start(csb.ConnectionString); + await using var dataSource = CreateDataSource(postmasterMock.ConnectionString); + await using var conn = await dataSource.OpenConnectionAsync(); + + var pgMock = await postmasterMock.WaitForServerConnection(); + await pgMock + .WriteParseComplete() + .WriteBindComplete() + .WriteRowDescription(new FieldDescription(Int4Oid)) + .WriteDataRow(new byte[4]) + .FlushAsync(); + + await using var cmd = new NpgsqlCommand("SELECT 1", conn); + await using (var reader = await cmd.ExecuteReaderAsync(Behavior | CommandBehavior.SingleRow)) + { + await reader.ReadAsync(); + // The second read will try to consume the whole resultset due to CommandBehavior.SingleRow + // Which will fail with timeout (and immediate connection break) since we didn't send anything else beside the first row + var ex = Assert.ThrowsAsync(async () => await reader.ReadAsync())!; + Assert.That(ex.InnerException, Is.TypeOf()); + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + } + } + #endregion #region Initialization / setup / teardown From 98ed04be107a11d62babbc1bbb91cab3c3b78e14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:31:39 +0000 Subject: [PATCH 047/155] Bump actions/checkout from 4 to 5 (#6174) --- .github/workflows/build.yml | 8 ++++---- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/native-aot.yml | 6 +++--- .github/workflows/rich-code-nav.yml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f10c34ea60..b38261e467 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: NuGet Cache uses: actions/cache@v4 @@ -140,7 +140,7 @@ jobs: sudo -u postgres psql -c "CREATE USER npgsql_tests_scram SUPERUSER PASSWORD 'npgsql_tests_scram'" # Uncomment the following to SSH into the agent running the build (https://github.com/mxschmitt/action-tmate) - #- uses: actions/checkout@v4 + #- uses: actions/checkout@v5 #- name: Setup tmate session # uses: mxschmitt/action-tmate@v3 @@ -343,7 +343,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: NuGet Cache uses: actions/cache@v4 @@ -383,7 +383,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.3.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2f34b67e27..4afb98c43a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index b3b2346a35..6f590ed720 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -93,7 +93,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 # - name: Setup nuget config # run: echo "$nuget_config" > NuGet.config @@ -127,7 +127,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 # - name: Setup nuget config # run: echo "$nuget_config" > NuGet.config @@ -154,7 +154,7 @@ jobs: shell: bash # Uncomment the following to SSH into the agent running the build (https://github.com/mxschmitt/action-tmate) - #- uses: actions/checkout@v4 + #- uses: actions/checkout@v5 #- name: Setup tmate session # uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index d0649277ca..f1987462a4 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: NuGet Cache uses: actions/cache@v4 From 6f8971ce5a811463c537376e8c70ffd9750cd396 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 20 Aug 2025 12:45:24 +0300 Subject: [PATCH 048/155] Fix concurrent NpgsqlDataSource.Dispose and Bootstrap (#6116) Fixes #6115 --- src/Npgsql/NpgsqlDataSource.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 7d78fca230..d8ff956141 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -497,8 +497,10 @@ protected virtual void DisposeBase() } _periodicPasswordProviderTimer?.Dispose(); - _setupMappingsSemaphore.Dispose(); MetricsReporter.Dispose(); + // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer + // Due to possible concurrent access, which might lead to deadlock + // See issue #6115 Clear(); } @@ -525,8 +527,10 @@ protected virtual async ValueTask DisposeAsyncBase() if (_periodicPasswordProviderTimer is not null) await _periodicPasswordProviderTimer.DisposeAsync().ConfigureAwait(false); - _setupMappingsSemaphore.Dispose(); MetricsReporter.Dispose(); + // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer + // Due to possible concurrent access, which might lead to deadlock + // See issue #6115 // TODO: async Clear, #4499 Clear(); From 19f466e3e12106b9e7a81e67d07c4df56467a861 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 20 Aug 2025 12:55:02 +0300 Subject: [PATCH 049/155] Set socket options before connecting to postgres (#6090) Closes #6013 --- src/Npgsql/Internal/NpgsqlConnector.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 7f720f5882..44b9504c71 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1217,6 +1217,9 @@ void Connect(NpgsqlTimeout timeout) try { + // Some options are not applied after the socket is open, see #6013 + SetSocketOptions(socket); + try { socket.Connect(endpoint); @@ -1235,7 +1238,6 @@ void Connect(NpgsqlTimeout timeout) if (write.Count is 0) throw new TimeoutException("Timeout during connection attempt"); socket.Blocking = true; - SetSocketOptions(socket); _socket = socket; ConnectedEndPoint = endpoint; return; @@ -1289,8 +1291,11 @@ Task GetHostAddressesAsync(CancellationToken ct) => var socket = new Socket(endpoint.AddressFamily, SocketType.Stream, protocolType); try { - await OpenSocketConnectionAsync(socket, endpoint, endpointTimeout, cancellationToken).ConfigureAwait(false); + // Some options are not applied after the socket is open, see #6013 SetSocketOptions(socket); + + await OpenSocketConnectionAsync(socket, endpoint, endpointTimeout, cancellationToken).ConfigureAwait(false); + _socket = socket; ConnectedEndPoint = endpoint; return; From 604036c68f820b56c6e4936c0f00ecad984fdc53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:16:28 +0200 Subject: [PATCH 050/155] Bump actions/setup-dotnet from 4.3.1 to 5.0.0 (#6182) --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/native-aot.yml | 4 ++-- .github/workflows/rich-code-nav.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b38261e467..6adbfffbaa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Build run: dotnet build -c ${{ matrix.config }} @@ -354,7 +354,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Pack run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true @@ -386,7 +386,7 @@ jobs: uses: actions/checkout@v5 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Pack run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4afb98c43a..5a465ad42e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -65,7 +65,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Build run: dotnet build -c Release diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 6f590ed720..ecc57d51a8 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -107,7 +107,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Write script run: echo "$AOT_Compat" > test-aot-compatibility.ps1 @@ -141,7 +141,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Start PostgreSQL run: | diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index f1987462a4..363eaaeb5d 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -23,7 +23,7 @@ jobs: ${{ runner.os }}-nuget- - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v4.3.1 + uses: actions/setup-dotnet@v5.0.0 - name: Build run: dotnet build --configuration Debug From 472259b05cd22edbb1dbdb040edd92f5cf778147 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:16:14 +0200 Subject: [PATCH 051/155] Bump BenchmarkDotNet from 0.13.12 to 0.15.2 (#6191) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f250d7c83..7e6ccaf1b8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,7 +37,7 @@ - + From 9af3db4edd4a61733dd477bca00171a06b647ba8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:18:49 +0200 Subject: [PATCH 052/155] Bump GitHubActionsTestLogger from 2.3.3 to 2.4.1 (#6192) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7e6ccaf1b8..1d094ba34b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,7 +32,7 @@ - + From 2108dfb8814634ca6dabe32325970ac9809d41c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:07:38 +0200 Subject: [PATCH 053/155] Bump Microsoft.Data.SqlClient from 5.2.2 to 6.1.1 (#6196) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d094ba34b..6115c0a079 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,7 +38,7 @@ - + From ffc3fba1b4f611390b2b23c4ca78d86de89bd272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:11:43 +0200 Subject: [PATCH 054/155] Move to PublicApiAnalyzers v4 (#6185) --- Directory.Packages.props | 2 +- src/Npgsql/PublicAPI.Shipped.txt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6115c0a079..0206250cab 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + diff --git a/src/Npgsql/PublicAPI.Shipped.txt b/src/Npgsql/PublicAPI.Shipped.txt index 3ec604ddc0..246a515cc2 100644 --- a/src/Npgsql/PublicAPI.Shipped.txt +++ b/src/Npgsql/PublicAPI.Shipped.txt @@ -1228,6 +1228,8 @@ NpgsqlTypes.NpgsqlBox.Width.get -> double NpgsqlTypes.NpgsqlCidr NpgsqlTypes.NpgsqlCidr.Address.get -> System.Net.IPAddress! NpgsqlTypes.NpgsqlCidr.Deconstruct(out System.Net.IPAddress! address, out byte netmask) -> void +NpgsqlTypes.NpgsqlCidr.Equals(NpgsqlTypes.NpgsqlCidr other) -> bool +NpgsqlTypes.NpgsqlInet.Equals(NpgsqlTypes.NpgsqlInet other) -> bool NpgsqlTypes.NpgsqlCidr.Netmask.get -> byte NpgsqlTypes.NpgsqlCidr.NpgsqlCidr() -> void NpgsqlTypes.NpgsqlCidr.NpgsqlCidr(string! addr) -> void @@ -1793,10 +1795,12 @@ override Npgsql.Schema.NpgsqlDbColumn.this[string! propertyName].get -> object? override NpgsqlTypes.NpgsqlBox.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlBox.GetHashCode() -> int override NpgsqlTypes.NpgsqlBox.ToString() -> string! +override NpgsqlTypes.NpgsqlCidr.GetHashCode() -> int override NpgsqlTypes.NpgsqlCidr.ToString() -> string! override NpgsqlTypes.NpgsqlCircle.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlCircle.GetHashCode() -> int override NpgsqlTypes.NpgsqlCircle.ToString() -> string! +override NpgsqlTypes.NpgsqlInet.GetHashCode() -> int override NpgsqlTypes.NpgsqlInet.ToString() -> string! override NpgsqlTypes.NpgsqlInterval.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlInterval.GetHashCode() -> int @@ -1886,10 +1890,14 @@ static NpgsqlTypes.NpgsqlBox.operator !=(NpgsqlTypes.NpgsqlBox x, NpgsqlTypes.Np static NpgsqlTypes.NpgsqlBox.operator ==(NpgsqlTypes.NpgsqlBox x, NpgsqlTypes.NpgsqlBox y) -> bool static NpgsqlTypes.NpgsqlCidr.explicit operator System.Net.IPAddress!(NpgsqlTypes.NpgsqlCidr cidr) -> System.Net.IPAddress! static NpgsqlTypes.NpgsqlCidr.implicit operator NpgsqlTypes.NpgsqlInet(NpgsqlTypes.NpgsqlCidr cidr) -> NpgsqlTypes.NpgsqlInet +static NpgsqlTypes.NpgsqlCidr.operator !=(NpgsqlTypes.NpgsqlCidr left, NpgsqlTypes.NpgsqlCidr right) -> bool +static NpgsqlTypes.NpgsqlCidr.operator ==(NpgsqlTypes.NpgsqlCidr left, NpgsqlTypes.NpgsqlCidr right) -> bool static NpgsqlTypes.NpgsqlCircle.operator !=(NpgsqlTypes.NpgsqlCircle x, NpgsqlTypes.NpgsqlCircle y) -> bool static NpgsqlTypes.NpgsqlCircle.operator ==(NpgsqlTypes.NpgsqlCircle x, NpgsqlTypes.NpgsqlCircle y) -> bool static NpgsqlTypes.NpgsqlInet.explicit operator System.Net.IPAddress!(NpgsqlTypes.NpgsqlInet inet) -> System.Net.IPAddress! static NpgsqlTypes.NpgsqlInet.implicit operator NpgsqlTypes.NpgsqlInet(System.Net.IPAddress! ip) -> NpgsqlTypes.NpgsqlInet +static NpgsqlTypes.NpgsqlInet.operator !=(NpgsqlTypes.NpgsqlInet left, NpgsqlTypes.NpgsqlInet right) -> bool +static NpgsqlTypes.NpgsqlInet.operator ==(NpgsqlTypes.NpgsqlInet left, NpgsqlTypes.NpgsqlInet right) -> bool static NpgsqlTypes.NpgsqlLine.operator !=(NpgsqlTypes.NpgsqlLine x, NpgsqlTypes.NpgsqlLine y) -> bool static NpgsqlTypes.NpgsqlLine.operator ==(NpgsqlTypes.NpgsqlLine x, NpgsqlTypes.NpgsqlLine y) -> bool static NpgsqlTypes.NpgsqlLogSequenceNumber.explicit operator NpgsqlTypes.NpgsqlLogSequenceNumber(ulong value) -> NpgsqlTypes.NpgsqlLogSequenceNumber @@ -1934,5 +1942,13 @@ static NpgsqlTypes.NpgsqlTsVector.Parse(string! value) -> NpgsqlTypes.NpgsqlTsVe static readonly Npgsql.NpgsqlFactory.Instance -> Npgsql.NpgsqlFactory! static readonly NpgsqlTypes.NpgsqlLogSequenceNumber.Invalid -> NpgsqlTypes.NpgsqlLogSequenceNumber static readonly NpgsqlTypes.NpgsqlRange.Empty -> NpgsqlTypes.NpgsqlRange +virtual Npgsql.NoticeEventHandler.Invoke(object! sender, Npgsql.NpgsqlNoticeEventArgs! e) -> void +virtual Npgsql.NotificationEventHandler.Invoke(object! sender, Npgsql.NpgsqlNotificationEventArgs! e) -> void virtual Npgsql.NpgsqlCommand.Clone() -> Npgsql.NpgsqlCommand! +virtual Npgsql.NpgsqlRowUpdatedEventHandler.Invoke(object! sender, Npgsql.NpgsqlRowUpdatedEventArgs! e) -> void +virtual Npgsql.NpgsqlRowUpdatingEventHandler.Invoke(object! sender, Npgsql.NpgsqlRowUpdatingEventArgs! e) -> void +virtual Npgsql.ProvideClientCertificatesCallback.Invoke(System.Security.Cryptography.X509Certificates.X509CertificateCollection! certificates) -> void +virtual Npgsql.ProvidePasswordCallback.Invoke(string! host, int port, string! database, string! username) -> string! virtual Npgsql.Replication.PgOutput.ReplicationTuple.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerator! +~override NpgsqlTypes.NpgsqlCidr.Equals(object obj) -> bool +~override NpgsqlTypes.NpgsqlInet.Equals(object obj) -> bool From 53b6028d0e782d97ae56dd1db37d0d3333e873f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:31:44 +0200 Subject: [PATCH 055/155] Move to NUnit v4 (#6183) --- .github/workflows/build.yml | 2 +- Directory.Packages.props | 5 +- .../Npgsql.DependencyInjection.Tests.csproj | 4 + .../Npgsql.NativeAotTests.csproj | 6 + test/Npgsql.PluginTests/GeoJSONTests.cs | 4 +- .../Npgsql.PluginTests.csproj | 4 + test/Npgsql.Tests/AuthenticationTests.cs | 14 +- test/Npgsql.Tests/BatchTests.cs | 8 +- test/Npgsql.Tests/BugTests.cs | 6 +- test/Npgsql.Tests/CommandBuilderTests.cs | 2 +- test/Npgsql.Tests/CommandParameterTests.cs | 22 +- test/Npgsql.Tests/CommandTests.cs | 42 +- test/Npgsql.Tests/ConnectionTests.cs | 34 +- test/Npgsql.Tests/CopyTests.cs | 6 +- test/Npgsql.Tests/DataAdapterTests.cs | 118 +++--- test/Npgsql.Tests/DataSourceTests.cs | 10 +- test/Npgsql.Tests/DataTypeNameTests.cs | 2 +- .../DistributedTransactionTests.cs | 48 +-- test/Npgsql.Tests/ExceptionTests.cs | 16 +- test/Npgsql.Tests/FunctionTests.cs | 22 +- test/Npgsql.Tests/LargeObjectTests.cs | 10 +- test/Npgsql.Tests/MultipleHostsTests.cs | 30 +- test/Npgsql.Tests/NestedDataReaderTests.cs | 8 +- test/Npgsql.Tests/NotificationTests.cs | 12 +- test/Npgsql.Tests/Npgsql.Tests.csproj | 4 + .../NpgsqlParameterCollectionTests.cs | 46 +-- test/Npgsql.Tests/NpgsqlParameterTests.cs | 362 +++++++++--------- test/Npgsql.Tests/PgPassEntryTests.cs | 20 +- test/Npgsql.Tests/PoolTests.cs | 4 +- test/Npgsql.Tests/PrepareTests.cs | 8 +- test/Npgsql.Tests/Properties/AssemblyInfo.cs | 2 +- test/Npgsql.Tests/ReaderNewSchemaTests.cs | 6 +- test/Npgsql.Tests/ReaderOldSchemaTests.cs | 16 +- test/Npgsql.Tests/ReaderTests.cs | 52 +-- .../Replication/CommonReplicationTests.cs | 2 +- .../Replication/PgOutputReplicationTests.cs | 18 +- .../Replication/PhysicalReplicationTests.cs | 4 +- .../TestDecodingReplicationTests.cs | 2 +- test/Npgsql.Tests/SchemaTests.cs | 8 +- test/Npgsql.Tests/SecurityTests.cs | 18 +- .../SnakeCaseNameTranslatorTests.cs | 8 +- test/Npgsql.Tests/StoredProcedureTests.cs | 20 +- .../TaskTimeoutAndCancellationTest.cs | 6 +- test/Npgsql.Tests/TestUtil.cs | 2 +- test/Npgsql.Tests/TracingTests.cs | 16 +- test/Npgsql.Tests/Types/ArrayTests.cs | 6 +- test/Npgsql.Tests/Types/ByteaTests.cs | 6 +- .../Types/CompositeHandlerTests.Read.cs | 18 +- .../Types/CompositeHandlerTests.Write.cs | 16 +- test/Npgsql.Tests/Types/CompositeTests.cs | 4 +- test/Npgsql.Tests/Types/DateTimeTests.cs | 16 +- test/Npgsql.Tests/Types/EnumTests.cs | 8 +- .../Npgsql.Tests/Types/FullTextSearchTests.cs | 16 +- test/Npgsql.Tests/Types/InternalTypeTests.cs | 22 +- test/Npgsql.Tests/Types/JsonDynamicTests.cs | 10 +- test/Npgsql.Tests/Types/JsonPathTests.cs | 2 +- test/Npgsql.Tests/Types/MultirangeTests.cs | 6 +- test/Npgsql.Tests/Types/RangeTests.cs | 76 ++-- test/Npgsql.Tests/Types/RecordTests.cs | 12 +- test/Npgsql.Tests/Types/TextTests.cs | 8 +- test/Npgsql.Tests/TypesTests.cs | 56 +-- 61 files changed, 683 insertions(+), 658 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6adbfffbaa..f37d7e432f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -310,7 +310,7 @@ 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" + 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.DependencyInjection.Tests --logger "GitHubActions;report-warnings=false" shell: bash diff --git a/Directory.Packages.props b/Directory.Packages.props index 0206250cab..d4671ecb06 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,12 +24,13 @@ - + + - + diff --git a/test/Npgsql.DependencyInjection.Tests/Npgsql.DependencyInjection.Tests.csproj b/test/Npgsql.DependencyInjection.Tests/Npgsql.DependencyInjection.Tests.csproj index 5f0006d79c..2f1f442547 100644 --- a/test/Npgsql.DependencyInjection.Tests/Npgsql.DependencyInjection.Tests.csproj +++ b/test/Npgsql.DependencyInjection.Tests/Npgsql.DependencyInjection.Tests.csproj @@ -4,6 +4,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj b/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj index 0757fb0dd6..7f9ce607ca 100644 --- a/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj +++ b/test/Npgsql.NativeAotTests/Npgsql.NativeAotTests.csproj @@ -18,4 +18,10 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/Npgsql.PluginTests/GeoJSONTests.cs b/test/Npgsql.PluginTests/GeoJSONTests.cs index 0a421eee01..287c1277bc 100644 --- a/test/Npgsql.PluginTests/GeoJSONTests.cs +++ b/test/Npgsql.PluginTests/GeoJSONTests.cs @@ -304,7 +304,7 @@ public async Task Import_geometry(TestData data) await using var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT field FROM {table}"; await using var reader = await cmd.ExecuteReaderAsync(); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); var actual = reader.GetValue(0); Assert.That(actual, Is.EqualTo(data.Geometry)); } @@ -341,7 +341,7 @@ public async Task Import_big_geometry() await using var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT field FROM {table}"; await using var reader = await cmd.ExecuteReaderAsync(); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); var actual = reader.GetValue(0); Assert.That(actual, Is.EqualTo(geometry)); } diff --git a/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj b/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj index 30dfb8ea16..499373bc63 100644 --- a/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj +++ b/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj @@ -5,6 +5,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Npgsql.Tests/AuthenticationTests.cs b/test/Npgsql.Tests/AuthenticationTests.cs index 90bdb79f00..157b1ee287 100644 --- a/test/Npgsql.Tests/AuthenticationTests.cs +++ b/test/Npgsql.Tests/AuthenticationTests.cs @@ -66,7 +66,7 @@ public async Task Password_provider([Values]bool async) using var dataSource = dataSourceBuilder.Build(); using var conn = async ? await dataSource.OpenConnectionAsync() : dataSource.OpenConnection(); - Assert.True(async ? asyncProviderCalled : syncProviderCalled, "Password_provider not used"); + Assert.That(async ? asyncProviderCalled : syncProviderCalled, "Password_provider not used"); } [Test] @@ -418,7 +418,7 @@ public async Task ProvidePasswordCallback_is_used() using (var conn = new NpgsqlConnection(builder.ConnectionString) { ProvidePasswordCallback = ProvidePasswordCallback }) { conn.Open(); - Assert.True(getPasswordDelegateWasCalled, "ProvidePasswordCallback delegate not used"); + Assert.That(getPasswordDelegateWasCalled, "ProvidePasswordCallback delegate not used"); // Do this again, since with multiplexing the very first connection attempt is done via // the non-multiplexing path, to surface any exceptions. @@ -427,7 +427,7 @@ public async Task ProvidePasswordCallback_is_used() getPasswordDelegateWasCalled = false; conn.Open(); Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); - Assert.True(getPasswordDelegateWasCalled, "ProvidePasswordCallback delegate not used"); + Assert.That(getPasswordDelegateWasCalled, "ProvidePasswordCallback delegate not used"); } string ProvidePasswordCallback(string host, int port, string database, string username) @@ -500,10 +500,10 @@ public void ProvidePasswordCallback_gets_correct_arguments() using (var conn = new NpgsqlConnection(builder.ConnectionString) { ProvidePasswordCallback = ProvidePasswordCallback }) { conn.Open(); - Assert.AreEqual(builder.Host, receivedHost); - Assert.AreEqual(builder.Port, receivedPort); - Assert.AreEqual(builder.Database, receivedDatabase); - Assert.AreEqual(builder.Username, receivedUsername); + Assert.That(receivedHost, Is.EqualTo(builder.Host)); + Assert.That(receivedPort, Is.EqualTo(builder.Port)); + Assert.That(receivedDatabase, Is.EqualTo(builder.Database)); + Assert.That(receivedUsername, Is.EqualTo(builder.Username)); } string ProvidePasswordCallback(string host, int port, string database, string username) diff --git a/test/Npgsql.Tests/BatchTests.cs b/test/Npgsql.Tests/BatchTests.cs index 4450635edb..960e6028f9 100644 --- a/test/Npgsql.Tests/BatchTests.cs +++ b/test/Npgsql.Tests/BatchTests.cs @@ -294,10 +294,10 @@ public async Task StatementOID() } [Test] - public void CanCreateParameter() => Assert.True(new NpgsqlBatchCommand().CanCreateParameter); + public void CanCreateParameter() => Assert.That(new NpgsqlBatchCommand().CanCreateParameter); [Test] - public void CreateParameter() => Assert.NotNull(new NpgsqlBatchCommand().CreateParameter()); + public void CreateParameter() => Assert.That(new NpgsqlBatchCommand().CreateParameter(), Is.Not.Null); #endregion NpgsqlBatchCommand @@ -702,7 +702,7 @@ await conn.ExecuteNonQueryAsync($@" // resources are referenced by the exception above, which is very likely to escape the using statement of the command. batch.Dispose(); var cmd2 = conn.CreateBatch(); - Assert.AreNotSame(cmd2, batch); + Assert.That(batch, Is.Not.SameAs(cmd2)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")] @@ -731,7 +731,7 @@ await conn.ExecuteNonQueryAsync($@" // resources are referenced by the exception above, which is very likely to escape the using statement of the command. batch.Dispose(); var cmd2 = conn.CreateBatch(); - Assert.AreNotSame(cmd2, batch); + Assert.That(batch, Is.Not.SameAs(cmd2)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4202")] diff --git a/test/Npgsql.Tests/BugTests.cs b/test/Npgsql.Tests/BugTests.cs index 2e3dfa97fc..8d46522c0f 100644 --- a/test/Npgsql.Tests/BugTests.cs +++ b/test/Npgsql.Tests/BugTests.cs @@ -1261,12 +1261,12 @@ public async Task Bug3649() using (var exporter = await conn.BeginBinaryExportAsync($"COPY {table} (value) TO STDIN (FORMAT binary)")) { await exporter.StartRowAsync(); - Assert.IsTrue(exporter.IsNull); + Assert.That(exporter.IsNull); await exporter.SkipAsync(); await exporter.StartRowAsync(); - Assert.AreEqual(1, await exporter.ReadAsync()); + Assert.That(await exporter.ReadAsync(), Is.EqualTo(1)); await exporter.StartRowAsync(); - Assert.AreEqual(2, await exporter.ReadAsync()); + Assert.That(await exporter.ReadAsync(), Is.EqualTo(2)); } } diff --git a/test/Npgsql.Tests/CommandBuilderTests.cs b/test/Npgsql.Tests/CommandBuilderTests.cs index a9fe980c5b..b47422e830 100644 --- a/test/Npgsql.Tests/CommandBuilderTests.cs +++ b/test/Npgsql.Tests/CommandBuilderTests.cs @@ -364,7 +364,7 @@ public async Task Get_update_command_with_column_aliases() using var cbCommandBuilder = new NpgsqlCommandBuilder(daDataAdapter); daDataAdapter.UpdateCommand = cbCommandBuilder.GetUpdateCommand(); - Assert.True(daDataAdapter.UpdateCommand.CommandText.Contains("SET \"cod\" = @p1, \"descr\" = @p2, \"data\" = @p3 WHERE ((\"cod\" = @p4) AND ((@p5 = 1 AND \"descr\" IS NULL) OR (\"descr\" = @p6)) AND ((@p7 = 1 AND \"data\" IS NULL) OR (\"data\" = @p8)))")); + Assert.That(daDataAdapter.UpdateCommand.CommandText.Contains("SET \"cod\" = @p1, \"descr\" = @p2, \"data\" = @p3 WHERE ((\"cod\" = @p4) AND ((@p5 = 1 AND \"descr\" IS NULL) OR (\"descr\" = @p6)) AND ((@p7 = 1 AND \"data\" IS NULL) OR (\"data\" = @p8)))")); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/2846")] diff --git a/test/Npgsql.Tests/CommandParameterTests.cs b/test/Npgsql.Tests/CommandParameterTests.cs index 6fc042bff9..3e758f2413 100644 --- a/test/Npgsql.Tests/CommandParameterTests.cs +++ b/test/Npgsql.Tests/CommandParameterTests.cs @@ -22,8 +22,8 @@ public async Task Input_and_output_parameters(CommandBehavior behavior) cmd.Parameters.Add(c); using (await cmd.ExecuteReaderAsync(behavior)) { - Assert.AreEqual(5, b.Value); - Assert.AreEqual(3, c.Value); + Assert.That(b.Value, Is.EqualTo(5)); + Assert.That(c.Value, Is.EqualTo(3)); } } @@ -128,20 +128,20 @@ public void Parameters_get_name() command.Parameters.Add(new NpgsqlParameter("Parameter4", DbType.DateTime)); var idbPrmtr = command.Parameters["Parameter1"]; - Assert.IsNotNull(idbPrmtr); + Assert.That(idbPrmtr, Is.Not.Null); command.Parameters[0].Value = 1; // Get by indexers. - Assert.AreEqual(":Parameter1", command.Parameters["Parameter1"].ParameterName); - Assert.AreEqual(":Parameter2", command.Parameters["Parameter2"].ParameterName); - Assert.AreEqual(":Parameter3", command.Parameters["Parameter3"].ParameterName); - Assert.AreEqual("Parameter4", command.Parameters["Parameter4"].ParameterName); //Should this work? + Assert.That(command.Parameters["Parameter1"].ParameterName, Is.EqualTo(":Parameter1")); + Assert.That(command.Parameters["Parameter2"].ParameterName, Is.EqualTo(":Parameter2")); + Assert.That(command.Parameters["Parameter3"].ParameterName, Is.EqualTo(":Parameter3")); + Assert.That(command.Parameters["Parameter4"].ParameterName, Is.EqualTo("Parameter4")); //Should this work? - Assert.AreEqual(":Parameter1", command.Parameters[0].ParameterName); - Assert.AreEqual(":Parameter2", command.Parameters[1].ParameterName); - Assert.AreEqual(":Parameter3", command.Parameters[2].ParameterName); - Assert.AreEqual("Parameter4", command.Parameters[3].ParameterName); + Assert.That(command.Parameters[0].ParameterName, Is.EqualTo(":Parameter1")); + Assert.That(command.Parameters[1].ParameterName, Is.EqualTo(":Parameter2")); + Assert.That(command.Parameters[2].ParameterName, Is.EqualTo(":Parameter3")); + Assert.That(command.Parameters[3].ParameterName, Is.EqualTo("Parameter4")); } [Test] diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs index 8cc36df10a..a5fb272851 100644 --- a/test/Npgsql.Tests/CommandTests.cs +++ b/test/Npgsql.Tests/CommandTests.cs @@ -522,7 +522,7 @@ public async Task Cursor_statement() while (dr.Read()) i++; - Assert.AreEqual(3, i); + Assert.That(i, Is.EqualTo(3)); dr.Close(); i = 0; @@ -530,7 +530,7 @@ public async Task Cursor_statement() var dr2 = command.ExecuteReader(); while (dr2.Read()) i++; - Assert.AreEqual(1, i); + Assert.That(i, Is.EqualTo(1)); dr2.Close(); command.CommandText = "close te;"; @@ -546,7 +546,7 @@ public async Task Cursor_move_RecordsAffected() command.ExecuteNonQuery(); command.CommandText = "MOVE FORWARD ALL IN curs"; var count = command.ExecuteNonQuery(); - Assert.AreEqual(3, count); + Assert.That(count, Is.EqualTo(3)); } #endregion @@ -707,7 +707,7 @@ public async Task Parameter_and_operator_unclear() command.Parameters.AddWithValue(":arr", new int[] {5, 4, 3, 2, 1}); await using var rdr = await command.ExecuteReaderAsync(); rdr.Read(); - Assert.AreEqual(rdr.GetInt32(0), 4); + Assert.That(rdr.GetInt32(0), Is.EqualTo(4)); } [Test] @@ -760,15 +760,15 @@ public async Task Statement_mapped_output_parameters(CommandBehavior behavior) await using var reader = await command.ExecuteReaderAsync(behavior); - Assert.AreEqual(4, command.Parameters["param1"].Value); - Assert.AreEqual(5, command.Parameters["param2"].Value); + Assert.That(command.Parameters["param1"].Value, Is.EqualTo(4)); + Assert.That(command.Parameters["param2"].Value, Is.EqualTo(5)); reader.Read(); - Assert.AreEqual(3, reader.GetInt32(0)); - Assert.AreEqual(4, reader.GetInt32(1)); - Assert.AreEqual(5, reader.GetInt32(2)); - Assert.AreEqual(6, reader.GetInt32(3)); + Assert.That(reader.GetInt32(0), Is.EqualTo(3)); + Assert.That(reader.GetInt32(1), Is.EqualTo(4)); + Assert.That(reader.GetInt32(2), Is.EqualTo(5)); + Assert.That(reader.GetInt32(3), Is.EqualTo(6)); } [Test] @@ -799,8 +799,8 @@ public async Task Bug1006158_output_parameters() _ = await command.ExecuteScalarAsync(); - Assert.AreEqual(3, command.Parameters[0].Value); - Assert.AreEqual(true, command.Parameters[1].Value); + Assert.That(command.Parameters[0].Value, Is.EqualTo(3)); + Assert.That(command.Parameters[1].Value, Is.EqualTo(true)); } [Test] @@ -813,16 +813,16 @@ public async Task Bug1010788_UpdateRowSource() var table = await CreateTempTable(conn, "id SERIAL PRIMARY KEY, name TEXT"); var command = new NpgsqlCommand($"SELECT * FROM {table}", conn); - Assert.AreEqual(UpdateRowSource.Both, command.UpdatedRowSource); + Assert.That(command.UpdatedRowSource, Is.EqualTo(UpdateRowSource.Both)); var cmdBuilder = new NpgsqlCommandBuilder(); var da = new NpgsqlDataAdapter(command); cmdBuilder.DataAdapter = da; - Assert.IsNotNull(da.SelectCommand); - Assert.IsNotNull(cmdBuilder.DataAdapter); + Assert.That(da.SelectCommand, Is.Not.Null); + Assert.That(cmdBuilder.DataAdapter, Is.Not.Null); var updateCommand = cmdBuilder.GetUpdateCommand(); - Assert.AreEqual(UpdateRowSource.None, updateCommand.UpdatedRowSource); + Assert.That(updateCommand.UpdatedRowSource, Is.EqualTo(UpdateRowSource.None)); } [Test] @@ -1146,10 +1146,10 @@ public async Task ExecuteReader_Throws_PostgresException([Values] bool async) ? await cmd.ExecuteReaderAsync() : cmd.ExecuteReader(); - Assert.IsTrue(async ? await reader.ReadAsync() : reader.Read()); + Assert.That(async ? await reader.ReadAsync() : reader.Read()); var value = reader.GetInt32(0); Assert.That(value, Is.EqualTo(1)); - Assert.IsFalse(async ? await reader.ReadAsync() : reader.Read()); + Assert.That(async ? await reader.ReadAsync() : reader.Read(), Is.False); var ex = async ? Assert.ThrowsAsync(async () => await reader.NextResultAsync()) : Assert.Throws(() => reader.NextResult()); @@ -1503,8 +1503,8 @@ public async Task Not_cancel_prepended_query([Values] bool failPrependedQuery) var cancellationRequestTask = postmasterMock.WaitForCancellationRequest().AsTask(); // Give 1 second to make sure we didn't send cancellation request await Task.Delay(1000); - Assert.IsFalse(cancelTask.IsCompleted); - Assert.IsFalse(cancellationRequestTask.IsCompleted); + Assert.That(cancelTask.IsCompleted, Is.False); + Assert.That(cancellationRequestTask.IsCompleted, Is.False); if (failPrependedQuery) { @@ -1622,7 +1622,7 @@ await server await connection.CloseAsync(); await connection.OpenAsync(); - Assert.AreSame(connector, connection.Connector); + Assert.That(connection.Connector, Is.SameAs(connector)); // We'll get new value after the next query reads ParameterStatus from the buffer Assert.That(connection.PostgresParameters, Does.Not.ContainKey("SomeKey").WithValue("SomeValue")); diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index ac97daf101..cda220a110 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -111,7 +111,7 @@ public async Task Broken_lifecycle([Values] bool openFromClose) Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); Assert.That(eventClosed, Is.True); Assert.That(conn.Connector is null); - Assert.AreEqual(0, conn.NpgsqlDataSource.Statistics.Total); + Assert.That(conn.NpgsqlDataSource.Statistics.Total, Is.EqualTo(0)); if (openFromClose) { @@ -123,8 +123,8 @@ public async Task Broken_lifecycle([Values] bool openFromClose) } Assert.DoesNotThrowAsync(conn.OpenAsync); - Assert.AreEqual(1, await conn.ExecuteScalarAsync("SELECT 1")); - Assert.AreEqual(1, conn.NpgsqlDataSource.Statistics.Total); + Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + Assert.That(conn.NpgsqlDataSource.Statistics.Total, Is.EqualTo(1)); Assert.DoesNotThrowAsync(conn.CloseAsync); } @@ -743,23 +743,23 @@ public async Task Set_Schemas_And_Load_Relevant_Types(string testSchema, string using var conn = await dataSource.OpenConnectionAsync(); if (enabled) { - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); if (testSchema == "public" || otherSchema == "public") { - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); } else { - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.False(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3"), Is.False); } } else { - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.True(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); } } finally @@ -945,7 +945,7 @@ public void Bug1011001() var cs1 = csb1.ToString(); var csb2 = new NpgsqlConnectionStringBuilder(cs1); var cs2 = csb2.ToString(); - Assert.IsTrue(cs1 == cs2); + Assert.That(cs1 == cs2); } [Test, IssueLink("https://github.com/npgsql/npgsql/pull/164")] @@ -953,7 +953,7 @@ public void Connection_State_is_Closed_when_disposed() { var c = new NpgsqlConnection(); c.Dispose(); - Assert.AreEqual(ConnectionState.Closed, c.State); + Assert.That(c.State, Is.EqualTo(ConnectionState.Closed)); } [Test] @@ -1131,9 +1131,9 @@ public async Task CloneWith_and_data_source_with_auth_callbacks([Values] bool as var sslClientAuthenticationOptions = new SslClientAuthenticationOptions(); clonedConnection.SslClientAuthenticationOptionsCallback!(sslClientAuthenticationOptions); - Assert.True(clientCertificatesCallbackCalled); + Assert.That(clientCertificatesCallbackCalled); sslClientAuthenticationOptions.RemoteCertificateValidationCallback!(null!, null, null, SslPolicyErrors.None); - Assert.True(userCertificateValidationCallbackCalled); + Assert.That(userCertificateValidationCallbackCalled); bool UserCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors) => userCertificateValidationCallbackCalled = true; @@ -1359,7 +1359,7 @@ await adminConn.ExecuteNonQueryAsync( await using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT * FROM foo"; await using var reader = await cmd.ExecuteReaderAsync(); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); using (var textReader = await reader.GetTextReaderAsync(0)) Assert.That(textReader.ReadToEnd(), Is.EqualTo(value)); @@ -1555,7 +1555,7 @@ public async Task Sync_open_blocked_same_thread() foreach (var sameThreadTask in sameThreadTasks) { - Assert.IsTrue(await sameThreadTask, "Synchronous open completed on different thread"); + Assert.That(await sameThreadTask, "Synchronous open completed on different thread"); } } diff --git a/test/Npgsql.Tests/CopyTests.cs b/test/Npgsql.Tests/CopyTests.cs index 73a2591195..4cddb400eb 100644 --- a/test/Npgsql.Tests/CopyTests.cs +++ b/test/Npgsql.Tests/CopyTests.cs @@ -362,9 +362,9 @@ public async Task Import_numeric() await using var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT field FROM {table}"; await using var reader = await cmd.ExecuteReaderAsync(); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetValue(0), Is.EqualTo(1234m)); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetValue(0), Is.EqualTo(5678m)); } @@ -753,7 +753,7 @@ public async Task Export_long_string() { var str = reader.Read(); Assert.That(str.Length, Is.EqualTo(len)); - Assert.True(str.AsSpan().IndexOfAnyExcept('x') is -1); + Assert.That(str.AsSpan().IndexOfAnyExcept('x') is -1); } } Assert.That(row, Is.EqualTo(100)); diff --git a/test/Npgsql.Tests/DataAdapterTests.cs b/test/Npgsql.Tests/DataAdapterTests.cs index 016e01b6b3..3c91521ae1 100644 --- a/test/Npgsql.Tests/DataAdapterTests.cs +++ b/test/Npgsql.Tests/DataAdapterTests.cs @@ -92,8 +92,8 @@ public async Task Insert_with_DataSet() var dr2 = new NpgsqlCommand($"SELECT field_int2, field_numeric, field_timestamp FROM {table}", conn).ExecuteReader(); dr2.Read(); - Assert.AreEqual(4, dr2[0]); - Assert.AreEqual(7.3000000M, dr2[1]); + Assert.That(dr2[0], Is.EqualTo(4)); + Assert.That(dr2[1], Is.EqualTo(7.3000000M)); dr2.Close(); } @@ -137,7 +137,7 @@ public async Task DataAdapter_update_return_value() var ds2 = ds.GetChanges()!; var daupdate = da.Update(ds2); - Assert.AreEqual(2, daupdate); + Assert.That(daupdate, Is.EqualTo(2)); } [Test] @@ -166,7 +166,7 @@ public async Task DataAdapter_update_return_value2() //## update should fail, and make a DBConcurrencyException var count = da.Update(ds); //## count is 1, even if the isn't updated in the database - Assert.AreEqual(0, count); + Assert.That(count, Is.EqualTo(0)); } [Test] @@ -180,12 +180,12 @@ public async Task Fill_with_empty_resultset() da.Fill(ds); - Assert.AreEqual(1, ds.Tables.Count); - Assert.AreEqual(4, ds.Tables[0].Columns.Count); - Assert.AreEqual("field_serial", ds.Tables[0].Columns[0].ColumnName); - Assert.AreEqual("field_int2", ds.Tables[0].Columns[1].ColumnName); - Assert.AreEqual("field_timestamp", ds.Tables[0].Columns[2].ColumnName); - Assert.AreEqual("field_numeric", ds.Tables[0].Columns[3].ColumnName); + Assert.That(ds.Tables.Count, Is.EqualTo(1)); + Assert.That(ds.Tables[0].Columns.Count, Is.EqualTo(4)); + Assert.That(ds.Tables[0].Columns[0].ColumnName, Is.EqualTo("field_serial")); + Assert.That(ds.Tables[0].Columns[1].ColumnName, Is.EqualTo("field_int2")); + Assert.That(ds.Tables[0].Columns[2].ColumnName, Is.EqualTo("field_timestamp")); + Assert.That(ds.Tables[0].Columns[3].ColumnName, Is.EqualTo("field_numeric")); } [Test] @@ -206,33 +206,33 @@ public async Task Fill_add_with_key() var field_timestamp = ds.Tables[0].Columns[2]; var field_numeric = ds.Tables[0].Columns[3]; - Assert.IsFalse(field_serial.AllowDBNull); - Assert.IsTrue(field_serial.AutoIncrement); - Assert.AreEqual("field_serial", field_serial.ColumnName); - Assert.AreEqual(typeof(int), field_serial.DataType); - Assert.AreEqual(0, field_serial.Ordinal); - Assert.IsTrue(field_serial.Unique); - - Assert.IsTrue(field_int2.AllowDBNull); - Assert.IsFalse(field_int2.AutoIncrement); - Assert.AreEqual("field_int2", field_int2.ColumnName); - Assert.AreEqual(typeof(short), field_int2.DataType); - Assert.AreEqual(1, field_int2.Ordinal); - Assert.IsFalse(field_int2.Unique); - - Assert.IsTrue(field_timestamp.AllowDBNull); - Assert.IsFalse(field_timestamp.AutoIncrement); - Assert.AreEqual("field_timestamp", field_timestamp.ColumnName); - Assert.AreEqual(typeof(DateTime), field_timestamp.DataType); - Assert.AreEqual(2, field_timestamp.Ordinal); - Assert.IsFalse(field_timestamp.Unique); - - Assert.IsTrue(field_numeric.AllowDBNull); - Assert.IsFalse(field_numeric.AutoIncrement); - Assert.AreEqual("field_numeric", field_numeric.ColumnName); - Assert.AreEqual(typeof(decimal), field_numeric.DataType); - Assert.AreEqual(3, field_numeric.Ordinal); - Assert.IsFalse(field_numeric.Unique); + Assert.That(field_serial.AllowDBNull, Is.False); + Assert.That(field_serial.AutoIncrement); + Assert.That(field_serial.ColumnName, Is.EqualTo("field_serial")); + Assert.That(field_serial.DataType, Is.EqualTo(typeof(int))); + Assert.That(field_serial.Ordinal, Is.EqualTo(0)); + Assert.That(field_serial.Unique); + + Assert.That(field_int2.AllowDBNull); + Assert.That(field_int2.AutoIncrement, Is.False); + Assert.That(field_int2.ColumnName, Is.EqualTo("field_int2")); + Assert.That(field_int2.DataType, Is.EqualTo(typeof(short))); + Assert.That(field_int2.Ordinal, Is.EqualTo(1)); + Assert.That(field_int2.Unique, Is.False); + + Assert.That(field_timestamp.AllowDBNull); + Assert.That(field_timestamp.AutoIncrement, Is.False); + Assert.That(field_timestamp.ColumnName, Is.EqualTo("field_timestamp")); + Assert.That(field_timestamp.DataType, Is.EqualTo(typeof(DateTime))); + Assert.That(field_timestamp.Ordinal, Is.EqualTo(2)); + Assert.That(field_timestamp.Unique, Is.False); + + Assert.That(field_numeric.AllowDBNull); + Assert.That(field_numeric.AutoIncrement, Is.False); + Assert.That(field_numeric.ColumnName, Is.EqualTo("field_numeric")); + Assert.That(field_numeric.DataType, Is.EqualTo(typeof(decimal))); + Assert.That(field_numeric.Ordinal, Is.EqualTo(3)); + Assert.That(field_numeric.Unique, Is.False); } [Test] @@ -252,21 +252,21 @@ public async Task Fill_add_columns() var field_timestamp = ds.Tables[0].Columns[2]; var field_numeric = ds.Tables[0].Columns[3]; - Assert.AreEqual("field_serial", field_serial.ColumnName); - Assert.AreEqual(typeof(int), field_serial.DataType); - Assert.AreEqual(0, field_serial.Ordinal); + Assert.That(field_serial.ColumnName, Is.EqualTo("field_serial")); + Assert.That(field_serial.DataType, Is.EqualTo(typeof(int))); + Assert.That(field_serial.Ordinal, Is.EqualTo(0)); - Assert.AreEqual("field_int2", field_int2.ColumnName); - Assert.AreEqual(typeof(short), field_int2.DataType); - Assert.AreEqual(1, field_int2.Ordinal); + Assert.That(field_int2.ColumnName, Is.EqualTo("field_int2")); + Assert.That(field_int2.DataType, Is.EqualTo(typeof(short))); + Assert.That(field_int2.Ordinal, Is.EqualTo(1)); - Assert.AreEqual("field_timestamp", field_timestamp.ColumnName); - Assert.AreEqual(typeof(DateTime), field_timestamp.DataType); - Assert.AreEqual(2, field_timestamp.Ordinal); + Assert.That(field_timestamp.ColumnName, Is.EqualTo("field_timestamp")); + Assert.That(field_timestamp.DataType, Is.EqualTo(typeof(DateTime))); + Assert.That(field_timestamp.Ordinal, Is.EqualTo(2)); - Assert.AreEqual("field_numeric", field_numeric.ColumnName); - Assert.AreEqual(typeof(decimal), field_numeric.DataType); - Assert.AreEqual(3, field_numeric.Ordinal); + Assert.That(field_numeric.ColumnName, Is.EqualTo("field_numeric")); + Assert.That(field_numeric.DataType, Is.EqualTo(typeof(decimal))); + Assert.That(field_numeric.Ordinal, Is.EqualTo(3)); } [Test] @@ -302,7 +302,7 @@ public async Task Update_letting_null_field_falue() da.Fill(ds); var dt = ds.Tables[0]; - Assert.IsNotNull(dt); + Assert.That(dt, Is.Not.Null); var dr = ds.Tables[0].Rows[^1]; dr["field_int2"] = 4; @@ -314,7 +314,7 @@ public async Task Update_letting_null_field_falue() using var dr2 = new NpgsqlCommand($"SELECT field_int2 FROM {table}", conn).ExecuteReader(); dr2.Read(); - Assert.AreEqual(4, dr2["field_int2"]); + Assert.That(dr2["field_int2"], Is.EqualTo(4)); } [Test] @@ -343,12 +343,12 @@ public async Task DoUpdateWithDataSet() var ds = new DataSet(); var da = new NpgsqlDataAdapter($"select * from {table}", conn); var cb = new NpgsqlCommandBuilder(da); - Assert.IsNotNull(cb); + Assert.That(cb, Is.Not.Null); da.Fill(ds); var dt = ds.Tables[0]; - Assert.IsNotNull(dt); + Assert.That(dt, Is.Not.Null); var dr = ds.Tables[0].Rows[^1]; @@ -361,7 +361,7 @@ public async Task DoUpdateWithDataSet() using var dr2 = new NpgsqlCommand($"select * from {table}", conn).ExecuteReader(); dr2.Read(); - Assert.AreEqual(4, dr2["field_int2"]); + Assert.That(dr2["field_int2"], Is.EqualTo(4)); } [Test] @@ -374,7 +374,7 @@ public async Task Insert_with_CommandBuilder_case_sensitive() var ds = new DataSet(); var da = new NpgsqlDataAdapter($"select * from {table}", conn); var builder = new NpgsqlCommandBuilder(da); - Assert.IsNotNull(builder); + Assert.That(builder, Is.Not.Null); da.Fill(ds); @@ -390,7 +390,7 @@ public async Task Insert_with_CommandBuilder_case_sensitive() using var dr2 = new NpgsqlCommand($"select * from {table}", conn).ExecuteReader(); dr2.Read(); - Assert.AreEqual(4, dr2[1]); + Assert.That(dr2[1], Is.EqualTo(4)); } [Test] @@ -449,7 +449,7 @@ public async Task DataAdapter_command_access() var da = new NpgsqlDataAdapter(); da.SelectCommand = command; System.Data.Common.DbDataAdapter common = da; - Assert.IsNotNull(common.SelectCommand); + Assert.That(common.SelectCommand, Is.Not.Null); } [Test, Description("Makes sure that the INSERT/UPDATE/DELETE commands are auto-populated on NpgsqlDataAdapter")] @@ -532,8 +532,8 @@ public async Task Load_DataTable() dt.Load(dr); dr.Close(); - Assert.AreEqual(5, dt.Columns[0].MaxLength); - Assert.AreEqual(5, dt.Columns[1].MaxLength); + Assert.That(dt.Columns[0].MaxLength, Is.EqualTo(5)); + Assert.That(dt.Columns[1].MaxLength, Is.EqualTo(5)); } public Task SetupTempTable(NpgsqlConnection conn) diff --git a/test/Npgsql.Tests/DataSourceTests.cs b/test/Npgsql.Tests/DataSourceTests.cs index 7e33d00991..f7aa537dd9 100644 --- a/test/Npgsql.Tests/DataSourceTests.cs +++ b/test/Npgsql.Tests/DataSourceTests.cs @@ -76,7 +76,7 @@ public async Task ExecuteReader_on_connectionless_command([Values] bool async) await using (var reader = async ? await command.ExecuteReaderAsync() : command.ExecuteReader()) { - Assert.True(reader.Read()); + Assert.That(reader.Read()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); } @@ -125,10 +125,10 @@ public async Task ExecuteReader_on_connectionless_batch([Values] bool async) using (var reader = async ? await batch.ExecuteReaderAsync() : batch.ExecuteReader()) { - Assert.True(reader.Read()); + Assert.That(reader.Read()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); - Assert.True(reader.NextResult()); - Assert.True(reader.Read()); + Assert.That(reader.NextResult()); + Assert.That(reader.Read()); Assert.That(reader.GetInt32(0), Is.EqualTo(2)); } @@ -318,7 +318,7 @@ public async Task Multiplexing_connectionless_command_open_connection() command.CommandText = "SELECT 1"; await using var reader = await command.ExecuteReaderAsync(); - Assert.True(reader.Read()); + Assert.That(reader.Read()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); } diff --git a/test/Npgsql.Tests/DataTypeNameTests.cs b/test/Npgsql.Tests/DataTypeNameTests.cs index fd366d8258..7ca6c669ce 100644 --- a/test/Npgsql.Tests/DataTypeNameTests.cs +++ b/test/Npgsql.Tests/DataTypeNameTests.cs @@ -12,7 +12,7 @@ public void MaxLengthDataTypeName() var name = new string('a', DataTypeName.NAMEDATALEN); var fullyQualifiedDataTypeName= $"public.{name}"; Assert.DoesNotThrow(() => new DataTypeName(fullyQualifiedDataTypeName)); - Assert.AreEqual(new DataTypeName(fullyQualifiedDataTypeName).Value, fullyQualifiedDataTypeName); + Assert.That(fullyQualifiedDataTypeName, Is.EqualTo(new DataTypeName(fullyQualifiedDataTypeName).Value)); } [Test] diff --git a/test/Npgsql.Tests/DistributedTransactionTests.cs b/test/Npgsql.Tests/DistributedTransactionTests.cs index 157e5ac112..aab4447ff2 100644 --- a/test/Npgsql.Tests/DistributedTransactionTests.cs +++ b/test/Npgsql.Tests/DistributedTransactionTests.cs @@ -179,12 +179,12 @@ public void Transaction_race([Values(false, true)] bool distributed) } catch (Exception ex) { - Assert.Fail( - @"Failed at iteration {0}. -Events: -{1} -Exception {2}", - i, FormatEventQueue(eventQueue), ex); + Assert.Fail($""" + Failed at iteration {i}. + Events: + {FormatEventQueue(eventQueue)} + Exception {ex} + """); } } } @@ -233,12 +233,12 @@ public void Connection_reuse_race_after_transaction([Values(false, true)] bool d } catch (Exception ex) { - Assert.Fail( - @"Failed at iteration {0}. -Events: -{1} -Exception {2}", - i, FormatEventQueue(eventQueue), ex); + Assert.Fail($""" + Failed at iteration {i}. + Events: + {FormatEventQueue(eventQueue)} + Exception {ex} + """); } } } @@ -287,12 +287,12 @@ public void Connection_reuse_race_after_rollback([Values(false, true)] bool dist } catch (Exception ex) { - Assert.Fail( - @"Failed at iteration {0}. -Events: -{1} -Exception {2}", - i, FormatEventQueue(eventQueue), ex); + Assert.Fail($""" + Failed at iteration {i}. + Events: + {FormatEventQueue(eventQueue)} + Exception {ex} + """); } } } @@ -365,12 +365,12 @@ public void Connection_reuse_race_chaining_transaction([Values(false, true)] boo } catch (Exception ex) { - Assert.Fail( - @"Failed at iteration {0}. -Events: -{1} -Exception {2}", - i, FormatEventQueue(eventQueue), ex); + Assert.Fail($""" + Failed at iteration {i}. + Events: + {FormatEventQueue(eventQueue)} + Exception {ex} + """); } } } diff --git a/test/Npgsql.Tests/ExceptionTests.cs b/test/Npgsql.Tests/ExceptionTests.cs index b58617ef52..ca667f8fd1 100644 --- a/test/Npgsql.Tests/ExceptionTests.cs +++ b/test/Npgsql.Tests/ExceptionTests.cs @@ -203,11 +203,11 @@ public void NpgsqlException_with_async() [Test] public void NpgsqlException_IsTransient() { - Assert.True(new NpgsqlException("", new IOException()).IsTransient); - Assert.True(new NpgsqlException("", new SocketException()).IsTransient); - Assert.True(new NpgsqlException("", new TimeoutException()).IsTransient); - Assert.False(new NpgsqlException().IsTransient); - Assert.False(new NpgsqlException("", new Exception("Inner Exception")).IsTransient); + Assert.That(new NpgsqlException("", new IOException()).IsTransient); + Assert.That(new NpgsqlException("", new SocketException()).IsTransient); + Assert.That(new NpgsqlException("", new TimeoutException()).IsTransient); + Assert.That(new NpgsqlException().IsTransient, Is.False); + Assert.That(new NpgsqlException("", new Exception("Inner Exception")).IsTransient, Is.False); } #if !NET9_0_OR_GREATER @@ -216,8 +216,8 @@ public void NpgsqlException_IsTransient() [Test] public void PostgresException_IsTransient() { - Assert.True(CreateWithSqlState("53300").IsTransient); - Assert.False(CreateWithSqlState("0").IsTransient); + Assert.That(CreateWithSqlState("53300").IsTransient); + Assert.That(CreateWithSqlState("0").IsTransient, Is.False); PostgresException CreateWithSqlState(string sqlState) { @@ -303,7 +303,7 @@ public void Base_exception_property_serialization() // Check virtual base properties, which can be incorrectly deserialized if overridden, because the base // Exception.GetObjectData() method writes the fields, not the properties (e.g. "_message" instead of "Message"). - Assert.That(ex.Data, Is.EquivalentTo((IDictionary?)info.GetValue("Data", typeof(IDictionary)))); + Assert.That(ex.Data, Is.EquivalentTo((IDictionary)info.GetValue("Data", typeof(IDictionary))!)); Assert.That(ex.HelpLink, Is.EqualTo(info.GetValue("HelpURL", typeof(string)))); Assert.That(ex.Message, Is.EqualTo(info.GetValue("Message", typeof(string)))); Assert.That(ex.Source, Is.EqualTo(info.GetValue("Source", typeof(string)))); diff --git a/test/Npgsql.Tests/FunctionTests.cs b/test/Npgsql.Tests/FunctionTests.cs index 9323dd2349..4c3b1e10aa 100644 --- a/test/Npgsql.Tests/FunctionTests.cs +++ b/test/Npgsql.Tests/FunctionTests.cs @@ -107,12 +107,12 @@ public async Task Named_parameters() command.Parameters.AddWithValue("sec", 4); var dt = (DateTime)(await command.ExecuteScalarAsync())!; - Assert.AreEqual(new DateTime(2015, 8, 1, 2, 3, 4), dt); + Assert.That(dt, Is.EqualTo(new DateTime(2015, 8, 1, 2, 3, 4))); command.Parameters[0].Value = 2014; command.Parameters[0].ParameterName = ""; // 2014 will be sent as a positional parameter dt = (DateTime)(await command.ExecuteScalarAsync())!; - Assert.AreEqual(new DateTime(2014, 8, 1, 2, 3, 4), dt); + Assert.That(dt, Is.EqualTo(new DateTime(2014, 8, 1, 2, 3, 4))); } [Test] @@ -174,7 +174,7 @@ public async Task CommandBehavior_SchemaOnly_support_function_call() var i = 0; while (dr.Read()) i++; - Assert.AreEqual(0, i); + Assert.That(i, Is.EqualTo(0)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/5820")] @@ -290,8 +290,8 @@ await conn.ExecuteNonQueryAsync( { await using var command = new NpgsqlCommand(@"""FunctionCaseSensitive""", conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -310,8 +310,8 @@ public async Task DeriveParameters_quote_characters_in_function_name() { await using var command = new NpgsqlCommand(function, conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -330,8 +330,8 @@ await conn.ExecuteNonQueryAsync( { await using var command = new NpgsqlCommand(@"""My.Dotted.Function""", conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -349,8 +349,8 @@ await conn.ExecuteNonQueryAsync( $"CREATE FUNCTION {function}(x int, y int, out sum int, out product int) AS 'SELECT $1 + $2, $1 * $2' LANGUAGE sql"); await using var command = new NpgsqlCommand(function, conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual("x", command.Parameters[0].ParameterName); - Assert.AreEqual("y", command.Parameters[1].ParameterName); + Assert.That(command.Parameters[0].ParameterName, Is.EqualTo("x")); + Assert.That(command.Parameters[1].ParameterName, Is.EqualTo("y")); } [Test] diff --git a/test/Npgsql.Tests/LargeObjectTests.cs b/test/Npgsql.Tests/LargeObjectTests.cs index 3d11dfd7b1..a252385be4 100644 --- a/test/Npgsql.Tests/LargeObjectTests.cs +++ b/test/Npgsql.Tests/LargeObjectTests.cs @@ -24,23 +24,23 @@ public void Test() stream.ReadExactly(buf2, 0, buf2.Length); Assert.That(buf.SequenceEqual(buf2)); - Assert.AreEqual(5, stream.Position); + Assert.That(stream.Position, Is.EqualTo(5)); - Assert.AreEqual(5, stream.Length); + Assert.That(stream.Length, Is.EqualTo(5)); stream.Seek(-1, System.IO.SeekOrigin.Current); - Assert.AreEqual((int)'o', stream.ReadByte()); + Assert.That(stream.ReadByte(), Is.EqualTo((int)'o')); manager.MaxTransferBlockSize = 3; stream.Write(buf, 0, buf.Length); stream.Seek(-5, System.IO.SeekOrigin.End); var buf3 = new byte[100]; - Assert.AreEqual(5, stream.Read(buf3, 0, 100)); + Assert.That(stream.Read(buf3, 0, 100), Is.EqualTo(5)); Assert.That(buf.SequenceEqual(buf3.Take(5))); stream.SetLength(43); - Assert.AreEqual(43, stream.Length); + Assert.That(stream.Length, Is.EqualTo(43)); } manager.Unlink(oid); diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index e09cbae401..f4026cc7f6 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -359,21 +359,21 @@ public async Task Connect_with_load_balancing() secondConnector = secondConnection.Connector!; } - Assert.AreNotSame(firstConnector, secondConnector); + Assert.That(secondConnector, Is.Not.SameAs(firstConnector)); await using (var firstBalancedConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(firstConnector, firstBalancedConnection.Connector); + Assert.That(firstBalancedConnection.Connector, Is.SameAs(firstConnector)); } await using (var secondBalancedConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(secondConnector, secondBalancedConnection.Connector); + Assert.That(secondBalancedConnection.Connector, Is.SameAs(secondConnector)); } await using (var thirdBalancedConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(firstConnector, thirdBalancedConnection.Connector); + Assert.That(thirdBalancedConnection.Connector, Is.SameAs(firstConnector)); } } @@ -403,7 +403,7 @@ public async Task Connect_without_load_balancing() } await using (var secondConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(firstConnector, secondConnection.Connector); + Assert.That(secondConnection.Connector, Is.SameAs(firstConnector)); } await using (var firstConnection = await dataSource.OpenConnectionAsync()) await using (var secondConnection = await dataSource.OpenConnectionAsync()) @@ -411,16 +411,16 @@ public async Task Connect_without_load_balancing() secondConnector = secondConnection.Connector!; } - Assert.AreNotSame(firstConnector, secondConnector); + Assert.That(secondConnector, Is.Not.SameAs(firstConnector)); await using (var firstUnbalancedConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(firstConnector, firstUnbalancedConnection.Connector); + Assert.That(firstUnbalancedConnection.Connector, Is.SameAs(firstConnector)); } await using (var secondUnbalancedConnection = await dataSource.OpenConnectionAsync()) { - Assert.AreSame(firstConnector, secondUnbalancedConnection.Connector); + Assert.That(secondUnbalancedConnection.Connector, Is.SameAs(firstConnector)); } } @@ -481,7 +481,7 @@ public async Task Connect_state_changing_hosts([Values] bool alwaysCheckHostStat } await using var thirdConnection = await dataSource.OpenConnectionAsync(TargetSessionAttributes.PreferPrimary); - Assert.AreSame(alwaysCheckHostState ? secondConnector : firstConnector, thirdConnection.Connector); + Assert.That(thirdConnection.Connector, Is.SameAs(alwaysCheckHostState ? secondConnector : firstConnector)); await firstServerTask; await secondServerTask; @@ -494,22 +494,22 @@ public void Database_state_cache_basic() var timeStamp = DateTime.UtcNow; dataSource.UpdateDatabaseState(DatabaseState.PrimaryReadWrite, timeStamp, TimeSpan.Zero); - Assert.AreEqual(DatabaseState.PrimaryReadWrite, dataSource.GetDatabaseState()); + Assert.That(dataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.PrimaryReadWrite)); // Update with the same timestamp - shouldn't change anything dataSource.UpdateDatabaseState(DatabaseState.Standby, timeStamp, TimeSpan.Zero); - Assert.AreEqual(DatabaseState.PrimaryReadWrite, dataSource.GetDatabaseState()); + Assert.That(dataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.PrimaryReadWrite)); // Update with a new timestamp timeStamp = timeStamp.AddSeconds(1); dataSource.UpdateDatabaseState(DatabaseState.PrimaryReadOnly, timeStamp, TimeSpan.Zero); - Assert.AreEqual(DatabaseState.PrimaryReadOnly, dataSource.GetDatabaseState()); + Assert.That(dataSource.GetDatabaseState(), Is.EqualTo(DatabaseState.PrimaryReadOnly)); // Expired state returns as Unknown (depending on ignoreExpiration) timeStamp = timeStamp.AddSeconds(1); dataSource.UpdateDatabaseState(DatabaseState.PrimaryReadWrite, timeStamp, TimeSpan.FromSeconds(-1)); - Assert.AreEqual(DatabaseState.Unknown, dataSource.GetDatabaseState(ignoreExpiration: false)); - Assert.AreEqual(DatabaseState.PrimaryReadWrite, dataSource.GetDatabaseState(ignoreExpiration: true)); + Assert.That(dataSource.GetDatabaseState(ignoreExpiration: false), Is.EqualTo(DatabaseState.Unknown)); + Assert.That(dataSource.GetDatabaseState(ignoreExpiration: true), Is.EqualTo(DatabaseState.PrimaryReadWrite)); } [Test] @@ -925,7 +925,7 @@ public void IntegrationTest([Values] bool loadBalancing, [Values] bool alwaysChe Assert.DoesNotThrowAsync(() => clientsTask); Assert.ThrowsAsync(() => onlyStandbyClient); Assert.ThrowsAsync(() => readOnlyClient); - Assert.AreEqual(125, queriesDone); + Assert.That(queriesDone, Is.EqualTo(125)); Task Client(NpgsqlMultiHostDataSource multiHostDataSource, TargetSessionAttributes targetSessionAttributes) { diff --git a/test/Npgsql.Tests/NestedDataReaderTests.cs b/test/Npgsql.Tests/NestedDataReaderTests.cs index 72553a6b5e..7e157c3426 100644 --- a/test/Npgsql.Tests/NestedDataReaderTests.cs +++ b/test/Npgsql.Tests/NestedDataReaderTests.cs @@ -199,15 +199,15 @@ public void GetBytes() Assert.That(nestedReader.GetBytes(0, 0, null, 0, 4), Is.EqualTo(3)); Assert.That(nestedReader.GetBytes(0, 0, buf, 0, 3), Is.EqualTo(3)); Assert.That(nestedReader.GetBytes(0, 0, buf, 0, 4), Is.EqualTo(3)); - CollectionAssert.AreEqual(new byte[] { 1, 2, 3, 0 }, buf); + Assert.That(buf, Is.EqualTo(new byte[] { 1, 2, 3, 0 }).AsCollection); buf = new byte[2]; Assert.That(nestedReader.GetBytes(0, 0, buf, 0, 2), Is.EqualTo(2)); - CollectionAssert.AreEqual(new byte[] { 1, 2 }, buf); + Assert.That(buf, Is.EqualTo(new byte[] { 1, 2 }).AsCollection); buf = new byte[2]; Assert.That(nestedReader.GetBytes(0, 1, buf, 1, 1), Is.EqualTo(1)); - CollectionAssert.AreEqual(new byte[] { 0, 2 }, buf); + Assert.That(buf, Is.EqualTo(new byte[] { 0, 2 }).AsCollection); Assert.That(nestedReader.GetBytes(0, 2, buf, 1, 1), Is.EqualTo(1)); - CollectionAssert.AreEqual(new byte[] { 0, 3 }, buf); + Assert.That(buf, Is.EqualTo(new byte[] { 0, 3 }).AsCollection); Assert.Throws(() => nestedReader.GetBytes(1, 0, buf, 0, 1)); Assert.Throws(() => nestedReader.GetBytes(0, 4, buf, 0, 1)); } diff --git a/test/Npgsql.Tests/NotificationTests.cs b/test/Npgsql.Tests/NotificationTests.cs index 9df9aba44d..5f6c11efcd 100644 --- a/test/Npgsql.Tests/NotificationTests.cs +++ b/test/Npgsql.Tests/NotificationTests.cs @@ -19,7 +19,7 @@ public void Notification() conn.ExecuteNonQuery($"LISTEN {notify}"); conn.Notification += (o, e) => receivedNotification = true; conn.ExecuteNonQuery($"NOTIFY {notify}"); - Assert.IsTrue(receivedNotification); + Assert.That(receivedNotification); } [Test, Description("Generates a notification that arrives after reader data that is already being read")] @@ -53,12 +53,12 @@ public async Task Notification_after_data() // Allow some time for the notification to get delivered await Task.Delay(2000); - Assert.IsTrue(reader.Read()); - Assert.AreEqual(1, reader.GetValue(0)); + Assert.That(reader.Read()); + Assert.That(reader.GetValue(0), Is.EqualTo(1)); } Assert.That(conn.ExecuteScalar("SELECT 1"), Is.EqualTo(1)); - Assert.IsTrue(receivedNotification); + Assert.That(receivedNotification); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/1024")] @@ -73,7 +73,7 @@ public void Wait() notifyingConn.ExecuteNonQuery($"NOTIFY {notify}"); conn.Notification += (o, e) => receivedNotification = true; Assert.That(conn.Wait(0), Is.EqualTo(true)); - Assert.IsTrue(receivedNotification); + Assert.That(receivedNotification); Assert.That(conn.ExecuteScalar("SELECT 1"), Is.EqualTo(1)); } @@ -106,7 +106,7 @@ public async Task WaitAsync() await notifyingConn.ExecuteNonQueryAsync($"NOTIFY {notify}"); conn.Notification += (o, e) => receivedNotification = true; await conn.WaitAsync(0); - Assert.IsTrue(receivedNotification); + Assert.That(receivedNotification); Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); } diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 5d100b68b5..1c300f8215 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -5,6 +5,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs index f6a188817b..901e34ece9 100644 --- a/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterCollectionTests.cs @@ -37,13 +37,13 @@ public void Clear() var c1 = new NpgsqlCommand(); var c2 = new NpgsqlCommand(); c1.Parameters.Add(p); - Assert.AreEqual(1, c1.Parameters.Count); - Assert.AreEqual(0, c2.Parameters.Count); + Assert.That(c1.Parameters.Count, Is.EqualTo(1)); + Assert.That(c2.Parameters.Count, Is.EqualTo(0)); c1.Parameters.Clear(); - Assert.AreEqual(0, c1.Parameters.Count); + Assert.That(c1.Parameters.Count, Is.EqualTo(0)); c2.Parameters.Add(p); - Assert.AreEqual(0, c1.Parameters.Count); - Assert.AreEqual(1, c2.Parameters.Count); + Assert.That(c1.Parameters.Count, Is.EqualTo(0)); + Assert.That(c2.Parameters.Count, Is.EqualTo(1)); } [Test] @@ -60,7 +60,7 @@ public void Hash_lookup_parameter_rename_bug() } // Make sure hash lookup is generated. - Assert.AreEqual(command.Parameters["p03"].ParameterName, "p03"); + Assert.That(command.Parameters["p03"].ParameterName, Is.EqualTo("p03")); // Rename the target parameter. command.Parameters["p03"].ParameterName = "a_new_name"; @@ -92,7 +92,7 @@ public void Hash_lookup_unnamed_parameter_rename_bug() } // Make sure hash lookup is generated. - Assert.AreEqual(command.Parameters["3"].ParameterName, "3"); + Assert.That(command.Parameters["3"].ParameterName, Is.EqualTo("3")); // Remove all parameters to clear hash lookup command.Parameters.Clear(); @@ -114,7 +114,7 @@ public void Remove_duplicate_parameter([Values(LookupThreshold, LookupThreshold } // Make sure lookup is generated. - Assert.AreEqual(command.Parameters["p02"].ParameterName, "p02"); + Assert.That(command.Parameters["p02"].ParameterName, Is.EqualTo("p02")); // Add uppercased version causing a list to be created. command.Parameters.AddWithValue("P02", NpgsqlDbType.Text, "String parameter value 2"); @@ -123,10 +123,10 @@ public void Remove_duplicate_parameter([Values(LookupThreshold, LookupThreshold command.Parameters.Remove(command.Parameters["p02"]); // Test whether we can still find the last added parameter, and if its index is correctly shifted in the lookup. - Assert.IsTrue(command.Parameters.IndexOf("p02") == count - 1); - Assert.IsTrue(command.Parameters.IndexOf("P02") == count - 1); + Assert.That(command.Parameters.IndexOf("p02") == count - 1); + Assert.That(command.Parameters.IndexOf("P02") == count - 1); // And finally test whether other parameters were also correctly shifted. - Assert.IsTrue(command.Parameters.IndexOf("p03") == 1); + Assert.That(command.Parameters.IndexOf("p03") == 1); } [Test] @@ -144,8 +144,8 @@ public void Remove_parameter([Values(LookupThreshold, LookupThreshold - 2)] int command.Parameters.Remove(command.Parameters["p02"]); // Make sure we cannot find it, also not case insensitively. - Assert.IsTrue(command.Parameters.IndexOf("p02") == -1); - Assert.IsTrue(command.Parameters.IndexOf("P02") == -1); + Assert.That(command.Parameters.IndexOf("p02") == -1); + Assert.That(command.Parameters.IndexOf("P02") == -1); } [Test] @@ -184,7 +184,7 @@ public void Correct_index_returned_for_duplicate_ParameterName([Values(LookupThr } // Make sure lookup is generated. - Assert.AreEqual(command.Parameters["parameter02"].ParameterName, "parameter02"); + Assert.That(command.Parameters["parameter02"].ParameterName, Is.EqualTo("parameter02")); // Add uppercased version. command.Parameters.AddWithValue("Parameter02", NpgsqlDbType.Text, "String parameter value 2"); @@ -193,14 +193,14 @@ public void Correct_index_returned_for_duplicate_ParameterName([Values(LookupThr command.Parameters.Insert(0, new NpgsqlParameter("ParameteR02", NpgsqlDbType.Text) { Value = "String parameter value 2" }); // Try to find the exact index. - Assert.IsTrue(command.Parameters.IndexOf("parameter02") == 2); - Assert.IsTrue(command.Parameters.IndexOf("Parameter02") == command.Parameters.Count - 1); - Assert.IsTrue(command.Parameters.IndexOf("ParameteR02") == 0); + Assert.That(command.Parameters.IndexOf("parameter02") == 2); + Assert.That(command.Parameters.IndexOf("Parameter02") == command.Parameters.Count - 1); + Assert.That(command.Parameters.IndexOf("ParameteR02") == 0); // This name does not exist so we expect the first case insensitive match to be returned. - Assert.IsTrue(command.Parameters.IndexOf("ParaMeteR02") == 0); + Assert.That(command.Parameters.IndexOf("ParaMeteR02") == 0); // And finally test whether other parameters were also correctly shifted. - Assert.IsTrue(command.Parameters.IndexOf("parameter03") == 3); + Assert.That(command.Parameters.IndexOf("parameter03") == 3); } [Test] @@ -345,8 +345,8 @@ public void Clean_name() param.ParameterName = null; // These should not throw exceptions - Assert.AreEqual(0, command.Parameters.IndexOf(param.ParameterName)); - Assert.AreEqual(NpgsqlParameter.PositionalName, param.ParameterName); + Assert.That(command.Parameters.IndexOf(param.ParameterName), Is.EqualTo(0)); + Assert.That(param.ParameterName, Is.EqualTo(NpgsqlParameter.PositionalName)); } [Test] @@ -354,10 +354,10 @@ public void Clone_sets_correct_collection() { var cmd = new NpgsqlCommand(); cmd.Parameters.Add(new NpgsqlParameter { TypedValue = 42 }); - Assert.AreSame(cmd.Parameters, cmd.Parameters.Single().Collection); + Assert.That(cmd.Parameters.Single().Collection, Is.SameAs(cmd.Parameters)); cmd = cmd.Clone(); - Assert.AreSame(cmd.Parameters, cmd.Parameters.Single().Collection); + Assert.That(cmd.Parameters.Single().Collection, Is.SameAs(cmd.Parameters)); } diff --git a/test/Npgsql.Tests/NpgsqlParameterTests.cs b/test/Npgsql.Tests/NpgsqlParameterTests.cs index 9a4610aadd..4965491c82 100644 --- a/test/Npgsql.Tests/NpgsqlParameterTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterTests.cs @@ -147,17 +147,17 @@ public void Setting_value_does_not_change_DbType() public void Constructor1() { var p = new NpgsqlParameter(); - Assert.AreEqual(DbType.Object, p.DbType, "DbType"); - Assert.AreEqual(ParameterDirection.Input, p.Direction, "Direction"); - Assert.IsFalse(p.IsNullable, "IsNullable"); - Assert.AreEqual(string.Empty, p.ParameterName, "ParameterName"); - Assert.AreEqual(0, p.Precision, "Precision"); - Assert.AreEqual(0, p.Scale, "Scale"); - Assert.AreEqual(0, p.Size, "Size"); - Assert.AreEqual(string.Empty, p.SourceColumn, "SourceColumn"); - Assert.AreEqual(DataRowVersion.Current, p.SourceVersion, "SourceVersion"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "NpgsqlDbType"); - Assert.IsNull(p.Value, "Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "Direction"); + Assert.That(p.IsNullable, Is.False, "IsNullable"); + Assert.That(p.ParameterName, Is.Empty, "ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "Scale"); + Assert.That(p.Size, Is.EqualTo(0), "Size"); + Assert.That(p.SourceColumn, Is.Empty, "SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "SourceVersion"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "NpgsqlDbType"); + Assert.That(p.Value, Is.Null, "Value"); } [Test] @@ -166,51 +166,51 @@ public void Constructor2_Value_DateTime() var value = new DateTime(2004, 8, 24); var p = new NpgsqlParameter("address", value); - Assert.AreEqual(DbType.DateTime2, p.DbType, "B:DbType"); - Assert.AreEqual(ParameterDirection.Input, p.Direction, "B:Direction"); - Assert.IsFalse(p.IsNullable, "B:IsNullable"); - Assert.AreEqual("address", p.ParameterName, "B:ParameterName"); - Assert.AreEqual(0, p.Precision, "B:Precision"); - Assert.AreEqual(0, p.Scale, "B:Scale"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime2), "B:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "B:Direction"); + Assert.That(p.IsNullable, Is.False, "B:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "B:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "B:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "B:Scale"); //Assert.AreEqual (0, p.Size, "B:Size"); - Assert.AreEqual(string.Empty, p.SourceColumn, "B:SourceColumn"); - Assert.AreEqual(DataRowVersion.Current, p.SourceVersion, "B:SourceVersion"); - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "B:NpgsqlDbType"); - Assert.AreEqual(value, p.Value, "B:Value"); + Assert.That(p.SourceColumn, Is.Empty, "B:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "B:SourceVersion"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "B:NpgsqlDbType"); + Assert.That(p.Value, Is.EqualTo(value), "B:Value"); } [Test] public void Constructor2_Value_DBNull() { var p = new NpgsqlParameter("address", DBNull.Value); - Assert.AreEqual(DbType.Object, p.DbType, "B:DbType"); - Assert.AreEqual(ParameterDirection.Input, p.Direction, "B:Direction"); - Assert.IsFalse(p.IsNullable, "B:IsNullable"); - Assert.AreEqual("address", p.ParameterName, "B:ParameterName"); - Assert.AreEqual(0, p.Precision, "B:Precision"); - Assert.AreEqual(0, p.Scale, "B:Scale"); - Assert.AreEqual(0, p.Size, "B:Size"); - Assert.AreEqual(string.Empty, p.SourceColumn, "B:SourceColumn"); - Assert.AreEqual(DataRowVersion.Current, p.SourceVersion, "B:SourceVersion"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "B:NpgsqlDbType"); - Assert.AreEqual(DBNull.Value, p.Value, "B:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "B:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "B:Direction"); + Assert.That(p.IsNullable, Is.False, "B:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "B:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "B:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "B:Scale"); + Assert.That(p.Size, Is.EqualTo(0), "B:Size"); + Assert.That(p.SourceColumn, Is.Empty, "B:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "B:SourceVersion"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "B:NpgsqlDbType"); + Assert.That(p.Value, Is.EqualTo(DBNull.Value), "B:Value"); } [Test] public void Constructor2_Value_null() { var p = new NpgsqlParameter("address", null); - Assert.AreEqual(DbType.Object, p.DbType, "A:DbType"); - Assert.AreEqual(ParameterDirection.Input, p.Direction, "A:Direction"); - Assert.IsFalse(p.IsNullable, "A:IsNullable"); - Assert.AreEqual("address", p.ParameterName, "A:ParameterName"); - Assert.AreEqual(0, p.Precision, "A:Precision"); - Assert.AreEqual(0, p.Scale, "A:Scale"); - Assert.AreEqual(0, p.Size, "A:Size"); - Assert.AreEqual(string.Empty, p.SourceColumn, "A:SourceColumn"); - Assert.AreEqual(DataRowVersion.Current, p.SourceVersion, "A:SourceVersion"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "A:NpgsqlDbType"); - Assert.IsNull(p.Value, "A:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "A:DbType"); + Assert.That(p.Direction, Is.EqualTo(ParameterDirection.Input), "A:Direction"); + Assert.That(p.IsNullable, Is.False, "A:IsNullable"); + Assert.That(p.ParameterName, Is.EqualTo("address"), "A:ParameterName"); + Assert.That(p.Precision, Is.EqualTo(0), "A:Precision"); + Assert.That(p.Scale, Is.EqualTo(0), "A:Scale"); + Assert.That(p.Size, Is.EqualTo(0), "A:Size"); + Assert.That(p.SourceColumn, Is.Empty, "A:SourceColumn"); + Assert.That(p.SourceVersion, Is.EqualTo(DataRowVersion.Current), "A:SourceVersion"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "A:NpgsqlDbType"); + Assert.That(p.Value, Is.Null, "A:Value"); } [Test] @@ -220,20 +220,20 @@ public void Constructor7() var p1 = new NpgsqlParameter("p1Name", NpgsqlDbType.Varchar, 20, "srcCol", ParameterDirection.InputOutput, false, 0, 0, DataRowVersion.Original, "foo"); - Assert.AreEqual(DbType.String, p1.DbType, "DbType"); - Assert.AreEqual(ParameterDirection.InputOutput, p1.Direction, "Direction"); - Assert.AreEqual(false, p1.IsNullable, "IsNullable"); + Assert.That(p1.DbType, Is.EqualTo(DbType.String), "DbType"); + Assert.That(p1.Direction, Is.EqualTo(ParameterDirection.InputOutput), "Direction"); + Assert.That(p1.IsNullable, Is.EqualTo(false), "IsNullable"); //Assert.AreEqual (999, p1.LocaleId, "#"); - Assert.AreEqual("p1Name", p1.ParameterName, "ParameterName"); - Assert.AreEqual(0, p1.Precision, "Precision"); - Assert.AreEqual(0, p1.Scale, "Scale"); - Assert.AreEqual(20, p1.Size, "Size"); - Assert.AreEqual("srcCol", p1.SourceColumn, "SourceColumn"); - Assert.AreEqual(false, p1.SourceColumnNullMapping, "SourceColumnNullMapping"); - Assert.AreEqual(DataRowVersion.Original, p1.SourceVersion, "SourceVersion"); - Assert.AreEqual(NpgsqlDbType.Varchar, p1.NpgsqlDbType, "NpgsqlDbType"); + Assert.That(p1.ParameterName, Is.EqualTo("p1Name"), "ParameterName"); + Assert.That(p1.Precision, Is.EqualTo(0), "Precision"); + Assert.That(p1.Scale, Is.EqualTo(0), "Scale"); + Assert.That(p1.Size, Is.EqualTo(20), "Size"); + Assert.That(p1.SourceColumn, Is.EqualTo("srcCol"), "SourceColumn"); + Assert.That(p1.SourceColumnNullMapping, Is.EqualTo(false), "SourceColumnNullMapping"); + Assert.That(p1.SourceVersion, Is.EqualTo(DataRowVersion.Original), "SourceVersion"); + Assert.That(p1.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Varchar), "NpgsqlDbType"); //Assert.AreEqual (3210, p1.NpgsqlValue, "#"); - Assert.AreEqual("foo", p1.Value, "Value"); + Assert.That(p1.Value, Is.EqualTo("foo"), "Value"); //Assert.AreEqual ("database", p1.XmlSchemaCollectionDatabase, "XmlSchemaCollectionDatabase"); //Assert.AreEqual ("name", p1.XmlSchemaCollectionName, "XmlSchemaCollectionName"); //Assert.AreEqual ("schema", p1.XmlSchemaCollectionOwningSchema, "XmlSchemaCollectionOwningSchema"); @@ -263,22 +263,22 @@ public void Clone() }; var actual = expected.Clone(); - Assert.AreEqual(expected.Value, actual.Value); - Assert.AreEqual(expected.ParameterName, actual.ParameterName); + Assert.That(actual.Value, Is.EqualTo(expected.Value)); + Assert.That(actual.ParameterName, Is.EqualTo(expected.ParameterName)); - Assert.AreEqual(expected.DbType, actual.DbType); - Assert.AreEqual(expected.NpgsqlDbType, actual.NpgsqlDbType); - Assert.AreEqual(expected.DataTypeName, actual.DataTypeName); + Assert.That(actual.DbType, Is.EqualTo(expected.DbType)); + Assert.That(actual.NpgsqlDbType, Is.EqualTo(expected.NpgsqlDbType)); + Assert.That(actual.DataTypeName, Is.EqualTo(expected.DataTypeName)); - Assert.AreEqual(expected.Direction, actual.Direction); - Assert.AreEqual(expected.IsNullable, actual.IsNullable); - Assert.AreEqual(expected.Precision, actual.Precision); - Assert.AreEqual(expected.Scale, actual.Scale); - Assert.AreEqual(expected.Size, actual.Size); + Assert.That(actual.Direction, Is.EqualTo(expected.Direction)); + Assert.That(actual.IsNullable, Is.EqualTo(expected.IsNullable)); + Assert.That(actual.Precision, Is.EqualTo(expected.Precision)); + Assert.That(actual.Scale, Is.EqualTo(expected.Scale)); + Assert.That(actual.Size, Is.EqualTo(expected.Size)); - Assert.AreEqual(expected.SourceVersion, actual.SourceVersion); - Assert.AreEqual(expected.SourceColumn, actual.SourceColumn); - Assert.AreEqual(expected.SourceColumnNullMapping, actual.SourceColumnNullMapping); + Assert.That(actual.SourceVersion, Is.EqualTo(expected.SourceVersion)); + Assert.That(actual.SourceColumn, Is.EqualTo(expected.SourceColumn)); + Assert.That(actual.SourceColumnNullMapping, Is.EqualTo(expected.SourceColumnNullMapping)); } [Test] @@ -305,23 +305,23 @@ public void Clone_generic() }; var actual = (NpgsqlParameter)expected.Clone(); - Assert.AreEqual(expected.Value, actual.Value); - Assert.AreEqual(expected.TypedValue, actual.TypedValue); - Assert.AreEqual(expected.ParameterName, actual.ParameterName); + Assert.That(actual.Value, Is.EqualTo(expected.Value)); + Assert.That(actual.TypedValue, Is.EqualTo(expected.TypedValue)); + Assert.That(actual.ParameterName, Is.EqualTo(expected.ParameterName)); - Assert.AreEqual(expected.DbType, actual.DbType); - Assert.AreEqual(expected.NpgsqlDbType, actual.NpgsqlDbType); - Assert.AreEqual(expected.DataTypeName, actual.DataTypeName); + Assert.That(actual.DbType, Is.EqualTo(expected.DbType)); + Assert.That(actual.NpgsqlDbType, Is.EqualTo(expected.NpgsqlDbType)); + Assert.That(actual.DataTypeName, Is.EqualTo(expected.DataTypeName)); - Assert.AreEqual(expected.Direction, actual.Direction); - Assert.AreEqual(expected.IsNullable, actual.IsNullable); - Assert.AreEqual(expected.Precision, actual.Precision); - Assert.AreEqual(expected.Scale, actual.Scale); - Assert.AreEqual(expected.Size, actual.Size); + Assert.That(actual.Direction, Is.EqualTo(expected.Direction)); + Assert.That(actual.IsNullable, Is.EqualTo(expected.IsNullable)); + Assert.That(actual.Precision, Is.EqualTo(expected.Precision)); + Assert.That(actual.Scale, Is.EqualTo(expected.Scale)); + Assert.That(actual.Size, Is.EqualTo(expected.Size)); - Assert.AreEqual(expected.SourceVersion, actual.SourceVersion); - Assert.AreEqual(expected.SourceColumn, actual.SourceColumn); - Assert.AreEqual(expected.SourceColumnNullMapping, actual.SourceColumnNullMapping); + Assert.That(actual.SourceVersion, Is.EqualTo(expected.SourceVersion)); + Assert.That(actual.SourceColumn, Is.EqualTo(expected.SourceColumn)); + Assert.That(actual.SourceColumnNullMapping, Is.EqualTo(expected.SourceColumnNullMapping)); } #endregion @@ -356,10 +356,10 @@ public void InferType_invalid_throws() catch (ArgumentException ex) { // The parameter data type of ... is invalid - Assert.AreEqual(typeof(ArgumentException), ex.GetType(), "#A2"); - Assert.IsNull(ex.InnerException, "#A3"); - Assert.IsNotNull(ex.Message, "#A4"); - Assert.IsNull(ex.ParamName, "#A5"); + Assert.That(ex.GetType(), Is.EqualTo(typeof(ArgumentException)), "#A2"); + Assert.That(ex.InnerException, Is.Null, "#A3"); + Assert.That(ex.Message, Is.Not.Null, "#A4"); + Assert.That(ex.ParamName, Is.Null, "#A5"); } } } @@ -368,14 +368,14 @@ public void InferType_invalid_throws() public void Parameter_null() { var param = new NpgsqlParameter("param", NpgsqlDbType.Numeric); - Assert.AreEqual(0, param.Scale, "#A1"); + Assert.That(param.Scale, Is.EqualTo(0), "#A1"); param.Value = DBNull.Value; - Assert.AreEqual(0, param.Scale, "#A2"); + Assert.That(param.Scale, Is.EqualTo(0), "#A2"); param = new NpgsqlParameter("param", NpgsqlDbType.Integer); - Assert.AreEqual(0, param.Scale, "#B1"); + Assert.That(param.Scale, Is.EqualTo(0), "#B1"); param.Value = DBNull.Value; - Assert.AreEqual(0, param.Scale, "#B2"); + Assert.That(param.Scale, Is.EqualTo(0), "#B2"); } [Test] @@ -388,53 +388,53 @@ public void Parameter_type() // assigned. The Type should be inferred everytime Value is assigned // If value is null or DBNull, then the current Type should be reset to Text. p = new NpgsqlParameter(); - Assert.AreEqual(DbType.String, p.DbType, "#A1"); - Assert.AreEqual(NpgsqlDbType.Text, p.NpgsqlDbType, "#A2"); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "#A1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#A2"); p.Value = DBNull.Value; - Assert.AreEqual(DbType.String, p.DbType, "#B1"); - Assert.AreEqual(NpgsqlDbType.Text, p.NpgsqlDbType, "#B2"); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "#B1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#B2"); p.Value = 1; - Assert.AreEqual(DbType.Int32, p.DbType, "#C1"); - Assert.AreEqual(NpgsqlDbType.Integer, p.NpgsqlDbType, "#C2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Int32), "#C1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer), "#C2"); p.Value = DBNull.Value; - Assert.AreEqual(DbType.String, p.DbType, "#D1"); - Assert.AreEqual(NpgsqlDbType.Text, p.NpgsqlDbType, "#D2"); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "#D1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#D2"); p.Value = new byte[] { 0x0a }; - Assert.AreEqual(DbType.Binary, p.DbType, "#E1"); - Assert.AreEqual(NpgsqlDbType.Bytea, p.NpgsqlDbType, "#E2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Binary), "#E1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#E2"); p.Value = null; - Assert.AreEqual(DbType.String, p.DbType, "#F1"); - Assert.AreEqual(NpgsqlDbType.Text, p.NpgsqlDbType, "#F2"); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "#F1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#F2"); p.Value = DateTime.Now; - Assert.AreEqual(DbType.DateTime, p.DbType, "#G1"); - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#G2"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime), "#G1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#G2"); p.Value = null; - Assert.AreEqual(DbType.String, p.DbType, "#H1"); - Assert.AreEqual(NpgsqlDbType.Text, p.NpgsqlDbType, "#H2"); + Assert.That(p.DbType, Is.EqualTo(DbType.String), "#H1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#H2"); // If DbType is set, then the NpgsqlDbType should not be // inferred from the value assigned. p = new NpgsqlParameter(); p.DbType = DbType.DateTime; - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#I1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I1"); p.Value = 1; - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#I2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I2"); p.Value = null; - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#I3"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I3"); p.Value = DBNull.Value; - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#I4"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I4"); // If NpgsqlDbType is set, then the DbType should not be // inferred from the value assigned. p = new NpgsqlParameter(); p.NpgsqlDbType = NpgsqlDbType.Bytea; - Assert.AreEqual(NpgsqlDbType.Bytea, p.NpgsqlDbType, "#J1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#J1"); p.Value = 1; - Assert.AreEqual(NpgsqlDbType.Bytea, p.NpgsqlDbType, "#J2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#J2"); p.Value = null; - Assert.AreEqual(NpgsqlDbType.Bytea, p.NpgsqlDbType, "#J3"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#J3"); p.Value = DBNull.Value; - Assert.AreEqual(NpgsqlDbType.Bytea, p.NpgsqlDbType, "#J4"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#J4"); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/5428")] @@ -452,24 +452,24 @@ public void ParameterName() { var p = new NpgsqlParameter(); p.ParameterName = "name"; - Assert.AreEqual("name", p.ParameterName, "#A:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#A:SourceColumn"); + Assert.That(p.ParameterName, Is.EqualTo("name"), "#A:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#A:SourceColumn"); p.ParameterName = null; - Assert.AreEqual(string.Empty, p.ParameterName, "#B:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#B:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#B:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#B:SourceColumn"); p.ParameterName = " "; - Assert.AreEqual(" ", p.ParameterName, "#C:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#C:SourceColumn"); + Assert.That(p.ParameterName, Is.EqualTo(" "), "#C:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#C:SourceColumn"); p.ParameterName = " name "; - Assert.AreEqual(" name ", p.ParameterName, "#D:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#D:SourceColumn"); + Assert.That(p.ParameterName, Is.EqualTo(" name "), "#D:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#D:SourceColumn"); p.ParameterName = string.Empty; - Assert.AreEqual(string.Empty, p.ParameterName, "#E:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#E:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#E:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#E:SourceColumn"); } [Test] @@ -480,59 +480,59 @@ public void ResetDbType() //Parameter with an assigned value but no DbType specified p = new NpgsqlParameter("foo", 42); p.ResetDbType(); - Assert.AreEqual(DbType.Int32, p.DbType, "#A:DbType"); - Assert.AreEqual(NpgsqlDbType.Integer, p.NpgsqlDbType, "#A:NpgsqlDbType"); - Assert.AreEqual(42, p.Value, "#A:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Int32), "#A:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer), "#A:NpgsqlDbType"); + Assert.That(p.Value, Is.EqualTo(42), "#A:Value"); p.DbType = DbType.DateTime; //assigning a DbType - Assert.AreEqual(DbType.DateTime, p.DbType, "#B:DbType1"); - Assert.AreEqual(NpgsqlDbType.TimestampTz, p.NpgsqlDbType, "#B:SqlDbType1"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime), "#B:DbType1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#B:SqlDbType1"); p.ResetDbType(); - Assert.AreEqual(DbType.Int32, p.DbType, "#B:DbType2"); - Assert.AreEqual(NpgsqlDbType.Integer, p.NpgsqlDbType, "#B:SqlDbtype2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Int32), "#B:DbType2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer), "#B:SqlDbtype2"); //Parameter with an assigned NpgsqlDbType but no specified value p = new NpgsqlParameter("foo", NpgsqlDbType.Integer); p.ResetDbType(); - Assert.AreEqual(DbType.Object, p.DbType, "#C:DbType"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#C:NpgsqlDbType"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#C:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#C:NpgsqlDbType"); p.NpgsqlDbType = NpgsqlDbType.TimestampTz; //assigning a NpgsqlDbType - Assert.AreEqual(DbType.DateTime, p.DbType, "#D:DbType1"); - Assert.AreEqual(NpgsqlDbType.TimestampTz, p.NpgsqlDbType, "#D:SqlDbType1"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime), "#D:DbType1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#D:SqlDbType1"); p.ResetDbType(); - Assert.AreEqual(DbType.Object, p.DbType, "#D:DbType2"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#D:SqlDbType2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#D:DbType2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#D:SqlDbType2"); p = new NpgsqlParameter(); p.Value = DateTime.MaxValue; - Assert.AreEqual(DbType.DateTime2, p.DbType, "#E:DbType1"); - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#E:SqlDbType1"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime2), "#E:DbType1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#E:SqlDbType1"); p.Value = null; p.ResetDbType(); - Assert.AreEqual(DbType.Object, p.DbType, "#E:DbType2"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#E:SqlDbType2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#E:DbType2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#E:SqlDbType2"); p = new NpgsqlParameter("foo", NpgsqlDbType.Varchar); p.Value = DateTime.MaxValue; p.ResetDbType(); - Assert.AreEqual(DbType.DateTime2, p.DbType, "#F:DbType"); - Assert.AreEqual(NpgsqlDbType.Timestamp, p.NpgsqlDbType, "#F:NpgsqlDbType"); - Assert.AreEqual(DateTime.MaxValue, p.Value, "#F:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime2), "#F:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#F:NpgsqlDbType"); + Assert.That(p.Value, Is.EqualTo(DateTime.MaxValue), "#F:Value"); p = new NpgsqlParameter("foo", NpgsqlDbType.Varchar); p.Value = DBNull.Value; p.ResetDbType(); - Assert.AreEqual(DbType.Object, p.DbType, "#G:DbType"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#G:NpgsqlDbType"); - Assert.AreEqual(DBNull.Value, p.Value, "#G:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#G:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#G:NpgsqlDbType"); + Assert.That(p.Value, Is.EqualTo(DBNull.Value), "#G:Value"); p = new NpgsqlParameter("foo", NpgsqlDbType.Varchar); p.Value = null; p.ResetDbType(); - Assert.AreEqual(DbType.Object, p.DbType, "#G:DbType"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#G:NpgsqlDbType"); - Assert.IsNull(p.Value, "#G:Value"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#G:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#G:NpgsqlDbType"); + Assert.That(p.Value, Is.Null, "#G:Value"); } [Test] @@ -545,24 +545,24 @@ public void SourceColumn() { var p = new NpgsqlParameter(); p.SourceColumn = "name"; - Assert.AreEqual(string.Empty, p.ParameterName, "#A:ParameterName"); - Assert.AreEqual("name", p.SourceColumn, "#A:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#A:ParameterName"); + Assert.That(p.SourceColumn, Is.EqualTo("name"), "#A:SourceColumn"); p.SourceColumn = null; - Assert.AreEqual(string.Empty, p.ParameterName, "#B:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#B:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#B:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#B:SourceColumn"); p.SourceColumn = " "; - Assert.AreEqual(string.Empty, p.ParameterName, "#C:ParameterName"); - Assert.AreEqual(" ", p.SourceColumn, "#C:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#C:ParameterName"); + Assert.That(p.SourceColumn, Is.EqualTo(" "), "#C:SourceColumn"); p.SourceColumn = " name "; - Assert.AreEqual(string.Empty, p.ParameterName, "#D:ParameterName"); - Assert.AreEqual(" name ", p.SourceColumn, "#D:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#D:ParameterName"); + Assert.That(p.SourceColumn, Is.EqualTo(" name "), "#D:SourceColumn"); p.SourceColumn = string.Empty; - Assert.AreEqual(string.Empty, p.ParameterName, "#E:ParameterName"); - Assert.AreEqual(string.Empty, p.SourceColumn, "#E:SourceColumn"); + Assert.That(p.ParameterName, Is.Empty, "#E:ParameterName"); + Assert.That(p.SourceColumn, Is.Empty, "#E:SourceColumn"); } [Test] @@ -570,8 +570,8 @@ public void Bug1011100_NpgsqlDbType() { var p = new NpgsqlParameter(); p.Value = DBNull.Value; - Assert.AreEqual(DbType.Object, p.DbType, "#A:DbType"); - Assert.AreEqual(NpgsqlDbType.Unknown, p.NpgsqlDbType, "#A:NpgsqlDbType"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#A:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#A:NpgsqlDbType"); // Now change parameter value. // Note that as we didn't explicitly specified a dbtype, the dbtype property should change when @@ -579,8 +579,8 @@ public void Bug1011100_NpgsqlDbType() p.Value = 8; - Assert.AreEqual(DbType.Int32, p.DbType, "#A:DbType"); - Assert.AreEqual(NpgsqlDbType.Integer, p.NpgsqlDbType, "#A:NpgsqlDbType"); + Assert.That(p.DbType, Is.EqualTo(DbType.Int32), "#A:DbType"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer), "#A:NpgsqlDbType"); //Assert.AreEqual(3510, p.Value, "#A:Value"); //p.NpgsqlDbType = NpgsqlDbType.Varchar; @@ -608,19 +608,19 @@ public void NpgsqlParameter_Clone() var newParam = param.Clone(); - Assert.AreEqual(param.Value, newParam.Value); - Assert.AreEqual(param.Precision, newParam.Precision); - Assert.AreEqual(param.Scale, newParam.Scale); - Assert.AreEqual(param.Size, newParam.Size); - Assert.AreEqual(param.Direction, newParam.Direction); - Assert.AreEqual(param.IsNullable, newParam.IsNullable); - Assert.AreEqual(param.ParameterName, newParam.ParameterName); - Assert.AreEqual(param.TrimmedName, newParam.TrimmedName); - Assert.AreEqual(param.SourceColumn, newParam.SourceColumn); - Assert.AreEqual(param.SourceVersion, newParam.SourceVersion); - Assert.AreEqual(param.NpgsqlValue, newParam.NpgsqlValue); - Assert.AreEqual(param.SourceColumnNullMapping, newParam.SourceColumnNullMapping); - Assert.AreEqual(param.NpgsqlValue, newParam.NpgsqlValue); + Assert.That(newParam.Value, Is.EqualTo(param.Value)); + Assert.That(newParam.Precision, Is.EqualTo(param.Precision)); + Assert.That(newParam.Scale, Is.EqualTo(param.Scale)); + Assert.That(newParam.Size, Is.EqualTo(param.Size)); + Assert.That(newParam.Direction, Is.EqualTo(param.Direction)); + Assert.That(newParam.IsNullable, Is.EqualTo(param.IsNullable)); + Assert.That(newParam.ParameterName, Is.EqualTo(param.ParameterName)); + Assert.That(newParam.TrimmedName, Is.EqualTo(param.TrimmedName)); + Assert.That(newParam.SourceColumn, Is.EqualTo(param.SourceColumn)); + Assert.That(newParam.SourceVersion, Is.EqualTo(param.SourceVersion)); + Assert.That(newParam.NpgsqlValue, Is.EqualTo(param.NpgsqlValue)); + Assert.That(newParam.SourceColumnNullMapping, Is.EqualTo(param.SourceColumnNullMapping)); + Assert.That(newParam.NpgsqlValue, Is.EqualTo(param.NpgsqlValue)); } @@ -632,7 +632,7 @@ public void Precision_via_interface() paramIface.Precision = 42; - Assert.AreEqual((byte)42, paramIface.Precision); + Assert.That(paramIface.Precision, Is.EqualTo((byte)42)); } [Test] @@ -643,7 +643,7 @@ public void Precision_via_base_class() paramBase.Precision = 42; - Assert.AreEqual((byte)42, paramBase.Precision); + Assert.That(paramBase.Precision, Is.EqualTo((byte)42)); } [Test] @@ -654,7 +654,7 @@ public void Scale_via_interface() paramIface.Scale = 42; - Assert.AreEqual((byte)42, paramIface.Scale); + Assert.That(paramIface.Scale, Is.EqualTo((byte)42)); } [Test] @@ -665,7 +665,7 @@ public void Scale_via_base_class() paramBase.Scale = 42; - Assert.AreEqual((byte)42, paramBase.Scale); + Assert.That(paramBase.Scale, Is.EqualTo((byte)42)); } [Test] diff --git a/test/Npgsql.Tests/PgPassEntryTests.cs b/test/Npgsql.Tests/PgPassEntryTests.cs index 9db518aabc..db78e893ad 100644 --- a/test/Npgsql.Tests/PgPassEntryTests.cs +++ b/test/Npgsql.Tests/PgPassEntryTests.cs @@ -13,11 +13,11 @@ public void Parses_well_formed_entry() var entry = PgPassFile.Entry.Parse(input); Assert.That(entry, Is.Not.Null); - Assert.That("test", Is.EqualTo(entry.Host)); - Assert.That(1234, Is.EqualTo(entry.Port)); - Assert.That("test2", Is.EqualTo(entry.Database)); - Assert.That("test3", Is.EqualTo(entry.Username)); - Assert.That("test4", Is.EqualTo(entry.Password)); + Assert.That(entry.Host, Is.EqualTo("test")); + Assert.That(entry.Port, Is.EqualTo(1234)); + Assert.That(entry.Database, Is.EqualTo("test2")); + Assert.That(entry.Username, Is.EqualTo("test3")); + Assert.That(entry.Password, Is.EqualTo("test4")); } [Test] @@ -36,11 +36,11 @@ public void Escaped_characters() var entry = PgPassFile.Entry.Parse(input); Assert.That(entry, Is.Not.Null); - Assert.That("t:est", Is.EqualTo(entry.Host)); - Assert.That(1234, Is.EqualTo(entry.Port)); - Assert.That("test2", Is.EqualTo(entry.Database)); - Assert.That("test3", Is.EqualTo(entry.Username)); - Assert.That("test\\4", Is.EqualTo(entry.Password)); + Assert.That(entry.Host, Is.EqualTo("t:est")); + Assert.That(entry.Port, Is.EqualTo(1234)); + Assert.That(entry.Database, Is.EqualTo("test2")); + Assert.That(entry.Username, Is.EqualTo("test3")); + Assert.That(entry.Password, Is.EqualTo("test\\4")); } [Test] diff --git a/test/Npgsql.Tests/PoolTests.cs b/test/Npgsql.Tests/PoolTests.cs index d9024dd0dd..af0fc27096 100644 --- a/test/Npgsql.Tests/PoolTests.cs +++ b/test/Npgsql.Tests/PoolTests.cs @@ -314,7 +314,7 @@ public void ClearPool(int iterations) } // Now have one connection in the pool - Assert.True(PoolManager.Pools.TryGetValue(connString, out var pool)); + Assert.That(PoolManager.Pools.TryGetValue(connString, out var pool)); AssertPoolState(pool, open: 1, idle: 1); NpgsqlConnection.ClearPool(conn); @@ -346,7 +346,7 @@ public void ClearPool_with_busy() NpgsqlConnection.ClearPool(conn); // conn is still busy but should get closed when returned to the pool - Assert.True(PoolManager.Pools.TryGetValue(connString, out pool)); + Assert.That(PoolManager.Pools.TryGetValue(connString, out pool)); AssertPoolState(pool, open: 1, idle: 0); } diff --git a/test/Npgsql.Tests/PrepareTests.cs b/test/Npgsql.Tests/PrepareTests.cs index 8a1c763e9a..fe01ef6fbc 100644 --- a/test/Npgsql.Tests/PrepareTests.cs +++ b/test/Npgsql.Tests/PrepareTests.cs @@ -755,20 +755,20 @@ public async Task Explicitly_prepared_statement_invalidation([Values] bool prepa // Since we've changed the table schema, the next execution of the prepared statement will error with 0A000 var exception = Assert.ThrowsAsync(() => command.ExecuteNonQueryAsync())!; Assert.That(exception.SqlState, Is.EqualTo(PostgresErrorCodes.FeatureNotSupported)); // cached plan must not change result type - Assert.IsFalse(command.IsPrepared); + Assert.That(command.IsPrepared, Is.False); if (unprepareAfterError) { // Just check that calling unprepare after error doesn't break anything await command.UnprepareAsync(); - Assert.IsFalse(command.IsPrepared); + Assert.That(command.IsPrepared, Is.False); } if (prepareAfterError) { // If we explicitly prepare after error, we should replace the previous prepared statement with a new one await command.PrepareAsync(); - Assert.IsTrue(command.IsPrepared); + Assert.That(command.IsPrepared); } // However, Npgsql should invalidate the prepared statement in this case, so the next execution should work @@ -777,7 +777,7 @@ public async Task Explicitly_prepared_statement_invalidation([Values] bool prepa if (!prepareAfterError) { // The command is unprepared, though. It's the user's responsibility to re-prepare if they wish. - Assert.False(command.IsPrepared); + Assert.That(command.IsPrepared, Is.False); } } diff --git a/test/Npgsql.Tests/Properties/AssemblyInfo.cs b/test/Npgsql.Tests/Properties/AssemblyInfo.cs index f7cdcd188d..89a1bb2e0d 100644 --- a/test/Npgsql.Tests/Properties/AssemblyInfo.cs +++ b/test/Npgsql.Tests/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using NUnit.Framework; -[assembly: Parallelizable(ParallelScope.Children), Timeout(30000)] +[assembly: Parallelizable(ParallelScope.Children)] [assembly: InternalsVisibleTo("Npgsql.PluginTests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100" + diff --git a/test/Npgsql.Tests/ReaderNewSchemaTests.cs b/test/Npgsql.Tests/ReaderNewSchemaTests.cs index 79b4b38ddb..2391cf54f0 100644 --- a/test/Npgsql.Tests/ReaderNewSchemaTests.cs +++ b/test/Npgsql.Tests/ReaderNewSchemaTests.cs @@ -757,9 +757,9 @@ public async Task GetColumnSchema_via_interface() var iface = (IDbColumnSchemaGenerator)reader; var schema = iface.GetColumnSchema(); - Assert.NotNull(schema); - Assert.AreEqual(1, schema.Count); - Assert.NotNull(schema[0]); + Assert.That(schema, Is.Not.Null); + Assert.That(schema.Count, Is.EqualTo(1)); + Assert.That(schema[0], Is.Not.Null); } #region Not supported diff --git a/test/Npgsql.Tests/ReaderOldSchemaTests.cs b/test/Npgsql.Tests/ReaderOldSchemaTests.cs index 43ac627f46..d6adcdf88f 100644 --- a/test/Npgsql.Tests/ReaderOldSchemaTests.cs +++ b/test/Npgsql.Tests/ReaderOldSchemaTests.cs @@ -124,12 +124,12 @@ await conn.ExecuteNonQueryAsync($@" var metadata = await GetSchemaTable(dr); var idRow = metadata!.Rows.OfType().FirstOrDefault(x => (string)x["ColumnName"] == "id"); - Assert.IsNotNull(idRow, "Unable to find metadata for id column"); + Assert.That(idRow, Is.Not.Null, "Unable to find metadata for id column"); var int2Row = metadata.Rows.OfType().FirstOrDefault(x => (string)x["ColumnName"] == "int2"); - Assert.IsNotNull(int2Row, "Unable to find metadata for int2 column"); + Assert.That(int2Row, Is.Not.Null, "Unable to find metadata for int2 column"); - Assert.IsFalse((bool)idRow!["IsReadonly"]); - Assert.IsTrue((bool)int2Row!["IsReadonly"]); + Assert.That((bool)idRow!["IsReadonly"], Is.False); + Assert.That((bool)int2Row!["IsReadonly"]); } // ReSharper disable once InconsistentNaming @@ -144,12 +144,12 @@ public async Task AllowDBNull() using var metadata = await GetSchemaTable(reader); var nullableRow = metadata!.Rows.OfType().FirstOrDefault(x => (string)x["ColumnName"] == "nullable"); - Assert.IsNotNull(nullableRow, "Unable to find metadata for nullable column"); + Assert.That(nullableRow, Is.Not.Null, "Unable to find metadata for nullable column"); var nonNullableRow = metadata.Rows.OfType().FirstOrDefault(x => (string)x["ColumnName"] == "non_nullable"); - Assert.IsNotNull(nonNullableRow, "Unable to find metadata for non_nullable column"); + Assert.That(nonNullableRow, Is.Not.Null, "Unable to find metadata for non_nullable column"); - Assert.IsTrue((bool)nullableRow!["AllowDBNull"]); - Assert.IsFalse((bool)nonNullableRow!["AllowDBNull"]); + Assert.That((bool)nullableRow!["AllowDBNull"]); + Assert.That((bool)nonNullableRow!["AllowDBNull"], Is.False); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/1027")] diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index 839ec5b610..432a9aa327 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -237,7 +237,7 @@ public async Task Get_string_with_parameter() using var dr = await command.ExecuteReaderAsync(Behavior); dr.Read(); var result = dr.GetString(0); - Assert.AreEqual(text, result); + Assert.That(result, Is.EqualTo(text)); } [Test] @@ -263,7 +263,7 @@ await conn.ExecuteNonQueryAsync($@" using var dr = await command.ExecuteReaderAsync(Behavior); dr.Read(); var result = dr.GetString(0); - Assert.AreEqual(test, result); + Assert.That(result, Is.EqualTo(test)); } [Test] @@ -496,7 +496,7 @@ public async Task ExecuteReader_getting_empty_resultset_with_output_parameter() param.Direction = ParameterDirection.Output; command.Parameters.Add(param); using var dr = await command.ExecuteReaderAsync(Behavior); - Assert.IsFalse(dr.NextResult()); + Assert.That(dr.NextResult(), Is.False); } [Test] @@ -644,7 +644,7 @@ await conn.ExecuteNonQueryAsync($@" // resources are referenced by the exception above, which is very likely to escape the using statement of the command. cmd.Dispose(); var cmd2 = conn.CreateCommand(); - Assert.AreNotSame(cmd2, cmd); + Assert.That(cmd, Is.Not.SameAs(cmd2)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")] @@ -672,7 +672,7 @@ await conn.ExecuteNonQueryAsync($@" // resources are referenced by the exception above, which is very likely to escape the using statement of the command. cmd.Dispose(); var cmd2 = conn.CreateCommand(); - Assert.AreNotSame(cmd2, cmd); + Assert.That(cmd, Is.Not.SameAs(cmd2)); } #region SchemaOnly @@ -694,8 +694,8 @@ public async Task SchemaOnly_next_result_beyond_end() using var cmd = new NpgsqlCommand($"SELECT * FROM {table}", conn); using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly); - Assert.False(reader.NextResult()); - Assert.False(reader.NextResult()); + Assert.That(reader.NextResult(), Is.False); + Assert.That(reader.NextResult(), Is.False); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4124")] @@ -879,7 +879,7 @@ public async Task HasRows_without_resultset() var table = await CreateTempTable(conn, "name TEXT"); using var command = new NpgsqlCommand($"DELETE FROM {table} WHERE name = 'unknown'", conn); using var reader = await command.ExecuteReaderAsync(Behavior); - Assert.IsFalse(reader.HasRows); + Assert.That(reader.HasRows, Is.False); } [Test] @@ -888,9 +888,9 @@ public async Task Interval_as_TimeSpan() using var conn = await OpenConnectionAsync(); using var command = new NpgsqlCommand("SELECT CAST('1 hour' AS interval) AS dauer", conn); using var dr = await command.ExecuteReaderAsync(Behavior); - Assert.IsTrue(dr.HasRows); - Assert.IsTrue(dr.Read()); - Assert.IsTrue(dr.HasRows); + Assert.That(dr.HasRows); + Assert.That(dr.Read()); + Assert.That(dr.HasRows); var ts = dr.GetTimeSpan(0); } @@ -946,7 +946,7 @@ public async Task SequentialBufferedSeekReread() //_ = rdr[5]; // uncomment lines for successful execution _ = rdr.IsDBNull(6); _ = rdr[6]; - Assert.True(rdr.IsDBNull(6)); + Assert.That(rdr.IsDBNull(6)); } } @@ -979,7 +979,7 @@ await pgMock .WriteCommandComplete() .WriteReadyForQuery() .FlushAsync(); - Assert.AreEqual(expected, await task); + Assert.That(await task, Is.EqualTo(expected)); } } @@ -1292,8 +1292,8 @@ public async Task Bug3772() reader.GetInt32(0); - Assert.Zero(reader.Connector.ReadBuffer.ReadBytesLeft); - Assert.NotZero(reader.Connector.ReadBuffer.ReadPosition); + Assert.That(reader.Connector.ReadBuffer.ReadBytesLeft, Is.Zero); + Assert.That(reader.Connector.ReadBuffer.ReadPosition, Is.Not.Zero); writeBuffer.WriteInt32(byteValue.Length); writeBuffer.WriteBytes(byteValue); @@ -1351,7 +1351,7 @@ public async Task Read_string_as_char() cmd.CommandText = "SELECT 'abcdefgh', 'ijklmnop'"; await using var reader = await cmd.ExecuteReaderAsync(Behavior); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetChar(0), Is.EqualTo('a')); if (Behavior == CommandBehavior.SequentialAccess) Assert.Throws(() => reader.GetChar(0)); @@ -1590,7 +1590,7 @@ public async Task GetStream_seek() var buffer = new byte[4]; await using var stream = reader.GetStream(0); - Assert.IsTrue(stream.CanSeek); + Assert.That(stream.CanSeek); var seekPosition = stream.Seek(-1, SeekOrigin.End); Assert.That(seekPosition, Is.EqualTo(stream.Length - 1)); @@ -1743,7 +1743,7 @@ public async Task TextReader_zero_length_column() cmd.CommandText = "SELECT ''"; await using var reader = await cmd.ExecuteReaderAsync(Behavior); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); using var textReader = reader.GetTextReader(0); Assert.That(textReader.Peek(), Is.EqualTo(-1)); @@ -1942,7 +1942,7 @@ await pgMock await using (var reader = await cmd.ExecuteReaderAsync(Behavior)) { // Successfully read the first row - Assert.True(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); // Attempt to read the second row - simulate blocking and cancellation @@ -1991,7 +1991,7 @@ await pgMock await using (var reader = await cmd.ExecuteReaderAsync(Behavior)) { // Successfully read the first row - Assert.True(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); // Attempt to read the second row - simulate blocking and cancellation @@ -2043,7 +2043,7 @@ await pgMock await using (var reader = await cmd.ExecuteReaderAsync(Behavior)) { // Successfully read the first resultset - Assert.True(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); // Attempt to advance to the second resultset - simulate blocking and cancellation @@ -2094,7 +2094,7 @@ await pgMock await using var reader = await cmd.ExecuteReaderAsync(Behavior); // Successfully read the first row - Assert.True(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); // Attempt to read the second row - simulate blocking and cancellation @@ -2139,7 +2139,7 @@ await pgMock await using var reader = await cmd.ExecuteReaderAsync(Behavior); // Successfully read the first resultset - Assert.True(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); Assert.That(reader.GetInt32(0), Is.EqualTo(1)); // Attempt to read the second row - simulate blocking and cancellation @@ -2247,11 +2247,11 @@ public async Task Cancel_multiplexing_disabled() await using var cmd = new NpgsqlCommand("SELECT generate_series(1, 100); SELECT generate_series(1, 100)", conn); await using var reader = await cmd.ExecuteReaderAsync(Behavior); var cancelledToken = new CancellationToken(canceled: true); - Assert.IsTrue(await reader.ReadAsync()); + Assert.That(await reader.ReadAsync()); while (await reader.ReadAsync(cancelledToken)) { } - Assert.IsTrue(await reader.NextResultAsync(cancelledToken)); + Assert.That(await reader.NextResultAsync(cancelledToken)); while (await reader.ReadAsync(cancelledToken)) { } - Assert.IsFalse(conn.Connector!.UserCancellationRequested); + Assert.That(conn.Connector!.UserCancellationRequested, Is.False); } #endregion Cancellation diff --git a/test/Npgsql.Tests/Replication/CommonReplicationTests.cs b/test/Npgsql.Tests/Replication/CommonReplicationTests.cs index 36a11b434a..c57280e184 100644 --- a/test/Npgsql.Tests/Replication/CommonReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/CommonReplicationTests.cs @@ -432,7 +432,7 @@ async Task GetCommitLsn(string valueString) // NpgsqlLogicalReplicationConnection // Begin Transaction, Insert, Commit Transaction for (var i = 0; i < 3; i++) - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); return messages.Current.Lsn; } diff --git a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs index 0186d4f0d8..e3cffd5766 100644 --- a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs @@ -1358,7 +1358,7 @@ await c.ExecuteNonQueryAsync($""" async Task AssertTransactionStart(IAsyncEnumerator messages) { - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); switch (messages.Current) { @@ -1379,13 +1379,13 @@ await c.ExecuteNonQueryAsync($""" async Task AssertTransactionCommit(IAsyncEnumerator messages) { - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); switch (messages.Current) { case StreamStopMessage: Assert.That(IsStreaming); - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); Assert.That(messages.Current, Is.TypeOf()); return; case CommitMessage: @@ -1398,10 +1398,10 @@ async Task AssertTransactionCommit(IAsyncEnumerator async Task AssertPrepare(IAsyncEnumerator enumerator) { - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); if (IsStreaming && enumerator.Current is StreamStopMessage) { - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); Assert.That(enumerator.Current, Is.TypeOf()); return (PrepareMessageBase)enumerator.Current!; } @@ -1413,16 +1413,16 @@ async Task AssertPrepare(IAsyncEnumerator NextMessage(IAsyncEnumerator enumerator, bool expectRelationMessage = false) where TExpected : PgOutputReplicationMessage { - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); if (IsStreaming && enumerator.Current is StreamStopMessage) { - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); Assert.That(enumerator.Current, Is.TypeOf()); - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); if (expectRelationMessage) { Assert.That(enumerator.Current, Is.TypeOf()); - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); } } diff --git a/test/Npgsql.Tests/Replication/PhysicalReplicationTests.cs b/test/Npgsql.Tests/Replication/PhysicalReplicationTests.cs index 59698b87ac..62c19451e9 100644 --- a/test/Npgsql.Tests/Replication/PhysicalReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/PhysicalReplicationTests.cs @@ -90,7 +90,7 @@ public Task Replication_with_slot() // other transactions possibly from system processes can // interfere here, inserting additional messages, but more // likely we'll get everything in one big chunk. - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); var message = messages.Current; Assert.That(message.WalStart, Is.EqualTo(info.XLogPos)); Assert.That(message.WalEnd, Is.GreaterThan(message.WalStart)); @@ -128,7 +128,7 @@ public async Task Replication_without_slot() // other transactions possibly from system processes can // interfere here, inserting additional messages, but more // likely we'll get everything in one big chunk. - Assert.True(await messages.MoveNextAsync()); + Assert.That(await messages.MoveNextAsync()); var message = messages.Current; Assert.That(message.WalStart, Is.EqualTo(info.XLogPos)); Assert.That(message.WalEnd, Is.GreaterThan(message.WalStart)); diff --git a/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs b/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs index 5d7c633f6c..3ecedfdfdd 100644 --- a/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs @@ -327,7 +327,7 @@ await c.ExecuteNonQueryAsync(@$" static async ValueTask NextMessage(IAsyncEnumerator enumerator) { - Assert.True(await enumerator.MoveNextAsync()); + Assert.That(await enumerator.MoveNextAsync()); return enumerator.Current!; } diff --git a/test/Npgsql.Tests/SchemaTests.cs b/test/Npgsql.Tests/SchemaTests.cs index 49f31eff19..ebe36269b0 100644 --- a/test/Npgsql.Tests/SchemaTests.cs +++ b/test/Npgsql.Tests/SchemaTests.cs @@ -257,7 +257,7 @@ public async Task ForeignKeys() { await using var conn = await OpenConnectionAsync(); var dt = await GetSchema(conn, "ForeignKeys"); - Assert.IsNotNull(dt); + Assert.That(dt, Is.Not.Null); } [Test] @@ -276,7 +276,7 @@ public async Task ParameterMarkerFormat() command.CommandText = $"SELECT * FROM {table} WHERE int=" + string.Format(parameterMarkerFormat, parameterName); command.Parameters.Add(new NpgsqlParameter(parameterName, 4)); await using var reader = await command.ExecuteReaderAsync(); - Assert.IsTrue(reader.Read()); + Assert.That(reader.Read()); } [Test] @@ -425,11 +425,11 @@ public async Task Unique_constraint() // Columns are not necessarily in the correct order var firstColumn = columns.FirstOrDefault(x => (string)x["column_name"] == "f1")!; - Assert.NotNull(firstColumn); + Assert.That(firstColumn, Is.Not.Null); Assert.That(firstColumn["ordinal_number"], Is.EqualTo(1)); var secondColumn = columns.FirstOrDefault(x => (string)x["column_name"] == "f2")!; - Assert.NotNull(secondColumn); + Assert.That(secondColumn, Is.Not.Null); Assert.That(secondColumn["ordinal_number"], Is.EqualTo(2)); } diff --git a/test/Npgsql.Tests/SecurityTests.cs b/test/Npgsql.Tests/SecurityTests.cs index 7f47ea8111..0ef9fd7b68 100644 --- a/test/Npgsql.Tests/SecurityTests.cs +++ b/test/Npgsql.Tests/SecurityTests.cs @@ -248,7 +248,7 @@ public async Task Connect_with_only_ssl_allowed_user([Values] bool multiplexing, csb.KeepAlive = keepAlive ? 10 : 0; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSslEncrypted); + Assert.That(conn.IsSslEncrypted); } catch (Exception e) when (!IsOnBuildServer) { @@ -277,7 +277,7 @@ public async Task Connect_with_only_non_ssl_allowed_user([Values] bool multiplex csb.KeepAlive = keepAlive ? 10 : 0; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsFalse(conn.IsSslEncrypted); + Assert.That(conn.IsSslEncrypted, Is.False); } catch (NpgsqlException ex) when (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && ex.InnerException is IOException) { @@ -319,7 +319,7 @@ public async Task DataSource_SslClientAuthenticationOptionsCallback_is_invoked([ Assert.That(ex.InnerException, Is.TypeOf()); } - Assert.IsTrue(callbackWasInvoked); + Assert.That(callbackWasInvoked); } [Test] @@ -348,7 +348,7 @@ public async Task Connection_SslClientAuthenticationOptionsCallback_is_invoked([ Assert.That(ex.InnerException, Is.TypeOf()); } - Assert.IsTrue(callbackWasInvoked); + Assert.That(callbackWasInvoked); } [Test] @@ -400,7 +400,7 @@ public async Task Bug4305_Secure([Values] bool async) try { conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSslEncrypted); + Assert.That(conn.IsSslEncrypted); } catch (Exception e) when (!IsOnBuildServer) { @@ -424,7 +424,7 @@ public async Task Bug4305_Secure([Values] bool async) await conn.CloseAsync(); await conn.OpenAsync(); - Assert.AreSame(originalConnector, conn.Connector); + Assert.That(conn.Connector, Is.SameAs(originalConnector)); } cmd.CommandText = "SELECT 1"; @@ -451,7 +451,7 @@ public async Task Bug4305_not_Secure([Values] bool async) try { conn = await dataSource.OpenConnectionAsync(); - Assert.IsFalse(conn.IsSslEncrypted); + Assert.That(conn.IsSslEncrypted, Is.False); } catch (Exception e) when (!IsOnBuildServer) { @@ -473,7 +473,7 @@ public async Task Bug4305_not_Secure([Values] bool async) await conn.CloseAsync(); await conn.OpenAsync(); - Assert.AreSame(originalConnector, conn.Connector); + Assert.That(conn.Connector, Is.SameAs(originalConnector)); cmd.CommandText = "SELECT 1"; if (async) @@ -494,7 +494,7 @@ public async Task Direct_ssl_negotiation() csb.SslNegotiation = SslNegotiation.Direct; }); await using var conn = await dataSource.OpenConnectionAsync(); - Assert.IsTrue(conn.IsSslEncrypted); + Assert.That(conn.IsSslEncrypted); } [Test] diff --git a/test/Npgsql.Tests/SnakeCaseNameTranslatorTests.cs b/test/Npgsql.Tests/SnakeCaseNameTranslatorTests.cs index 52de32bccf..9a64ccdaa2 100644 --- a/test/Npgsql.Tests/SnakeCaseNameTranslatorTests.cs +++ b/test/Npgsql.Tests/SnakeCaseNameTranslatorTests.cs @@ -66,9 +66,9 @@ public void TurkeyTest() const string clrName = "IPhone"; const string expected = "i_phone"; - Assert.AreEqual(expected, translator.TranslateMemberName(clrName)); - Assert.AreEqual(expected, translator.TranslateTypeName(clrName)); - Assert.AreEqual(expected, legacyTranslator.TranslateMemberName(clrName)); - Assert.AreEqual(expected, legacyTranslator.TranslateTypeName(clrName)); + Assert.That(translator.TranslateMemberName(clrName), Is.EqualTo(expected)); + Assert.That(translator.TranslateTypeName(clrName), Is.EqualTo(expected)); + Assert.That(legacyTranslator.TranslateMemberName(clrName), Is.EqualTo(expected)); + Assert.That(legacyTranslator.TranslateTypeName(clrName), Is.EqualTo(expected)); } } diff --git a/test/Npgsql.Tests/StoredProcedureTests.cs b/test/Npgsql.Tests/StoredProcedureTests.cs index 84acb51b36..ae13fa015c 100644 --- a/test/Npgsql.Tests/StoredProcedureTests.cs +++ b/test/Npgsql.Tests/StoredProcedureTests.cs @@ -207,8 +207,8 @@ LANGUAGE plpgsql }; await batch.ExecuteNonQueryAsync(); - Assert.AreEqual(1, batch.BatchCommands[0].Parameters[1].Value); - Assert.AreEqual(1, batch.BatchCommands[1].Parameters[1].Value); + Assert.That(batch.BatchCommands[0].Parameters[1].Value, Is.EqualTo(1)); + Assert.That(batch.BatchCommands[1].Parameters[1].Value, Is.EqualTo(1)); } #region DeriveParameters @@ -295,8 +295,8 @@ public async Task DeriveParameters_procedure_with_case_sensitive_name() { await using var command = new NpgsqlCommand(@"""ProcedureCaseSensitive""", conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -315,8 +315,8 @@ public async Task DeriveParameters_quote_characters_in_function_name() { await using var command = new NpgsqlCommand(sproc, conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -335,8 +335,8 @@ await conn.ExecuteNonQueryAsync( { await using var command = new NpgsqlCommand(@"""My.Dotted.Procedure""", conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual(NpgsqlDbType.Integer, command.Parameters[0].NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Text, command.Parameters[1].NpgsqlDbType); + Assert.That(command.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer)); + Assert.That(command.Parameters[1].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text)); } finally { @@ -355,8 +355,8 @@ await conn.ExecuteNonQueryAsync( $"CREATE PROCEDURE {sproc}(x int, y int, out sum int, out product int) AS 'SELECT $1 + $2, $1 * $2' LANGUAGE sql"); await using var command = new NpgsqlCommand(sproc, conn) { CommandType = CommandType.StoredProcedure }; NpgsqlCommandBuilder.DeriveParameters(command); - Assert.AreEqual("x", command.Parameters[0].ParameterName); - Assert.AreEqual("y", command.Parameters[1].ParameterName); + Assert.That(command.Parameters[0].ParameterName, Is.EqualTo("x")); + Assert.That(command.Parameters[1].ParameterName, Is.EqualTo("y")); } [Test] diff --git a/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs b/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs index e3759d35e9..f90bd1dd92 100644 --- a/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs +++ b/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs @@ -21,7 +21,7 @@ async Task GetResultTaskAsync(int timeout, CancellationToken ct) [Test] public async Task SuccessfulResultTaskAsync() => - Assert.AreEqual(TestResultValue, await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetResultTaskAsync(10, ct), NpgsqlTimeout.Infinite, CancellationToken.None)); + Assert.That(await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetResultTaskAsync(10, ct), NpgsqlTimeout.Infinite, CancellationToken.None), Is.EqualTo(TestResultValue)); [Test] public async Task SuccessfulVoidTaskAsync() => @@ -112,7 +112,7 @@ void OnUnobservedTaskException(object? source, UnobservedTaskExceptionEventArgs await test(() => unobservedTaskException); // Verify the unobserved Task exception event has not been received. - Assert.IsNull(unobservedTaskException, unobservedTaskException?.Message); + Assert.That(unobservedTaskException, Is.Null, unobservedTaskException?.Message); } finally { @@ -157,6 +157,6 @@ await TaskTimeoutAndCancellation.ExecuteAsync( { // Expected due to preemptive cancellation. } - Assert.False(nonCancellableTask.IsCompleted); + Assert.That(nonCancellableTask.IsCompleted, Is.False); } } diff --git a/test/Npgsql.Tests/TestUtil.cs b/test/Npgsql.Tests/TestUtil.cs index 0cf7a6a75f..fc0b5404d8 100644 --- a/test/Npgsql.Tests/TestUtil.cs +++ b/test/Npgsql.Tests/TestUtil.cs @@ -428,7 +428,7 @@ internal static void AssertLoggingStateContains( (LogLevel Level, EventId Id, string Message, object? State, Exception? Exception) log, string key, T value) - => Assert.That(log.State, Contains.Item(new KeyValuePair(key, value))); + => Assert.That(log.State as IEnumerable>, Contains.Item(new KeyValuePair(key, value))); internal static void AssertLoggingStateDoesNotContain( (LogLevel Level, EventId Id, string Message, object? State, Exception? Exception) log, diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 5cf0fca200..74e43f63a8 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -59,7 +59,7 @@ static void ValidateActivity(Activity activity, NpgsqlConnection conn, bool isMu var expectedTagCount = conn.Settings.Port == 5432 ? 8 : 9; Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); - Assert.IsFalse(activity.TagObjects.Any(x => x.Key == "db.statement")); + Assert.That(activity.TagObjects.Any(x => x.Key == "db.statement"), Is.False); var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); Assert.That(systemTag.Value, Is.EqualTo("postgresql")); @@ -79,7 +79,7 @@ static void ValidateActivity(Activity activity, NpgsqlConnection conn, bool isMu Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); } else - Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); } } @@ -140,7 +140,7 @@ public async Task Basic_query([Values] bool async, [Values] bool batch) Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); } else - Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); } [Test] @@ -182,10 +182,10 @@ public async Task Error_open([Values] bool async) Assert.That(exceptionTypeTag.Value, Is.EqualTo(ex.GetType().FullName)); var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); - StringAssert.Contains(ex.Message, (string)exceptionMessageTag.Value!); + Assert.That((string)exceptionMessageTag.Value!, Does.Contain(ex.Message)); var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); - StringAssert.Contains(ex.Message, (string)exceptionStacktraceTag.Value!); + Assert.That((string)exceptionStacktraceTag.Value!, Does.Contain(ex.Message)); var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); Assert.That(exceptionEscapedTag.Value, Is.True); @@ -239,10 +239,10 @@ public async Task Error_query([Values] bool async, [Values] bool batch) Assert.That(exceptionTypeTag.Value, Is.EqualTo("Npgsql.PostgresException")); var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); - StringAssert.Contains("relation \"non_existing_table\" does not exist", (string)exceptionMessageTag.Value!); + Assert.That((string)exceptionMessageTag.Value!, Does.Contain("relation \"non_existing_table\" does not exist")); var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); - StringAssert.Contains("relation \"non_existing_table\" does not exist", (string)exceptionStacktraceTag.Value!); + Assert.That((string)exceptionStacktraceTag.Value!, Does.Contain("relation \"non_existing_table\" does not exist")); var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); Assert.That(exceptionEscapedTag.Value, Is.True); @@ -271,7 +271,7 @@ public async Task Error_query([Values] bool async, [Values] bool batch) Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); } else - Assert.IsTrue(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); } [Test] diff --git a/test/Npgsql.Tests/Types/ArrayTests.cs b/test/Npgsql.Tests/Types/ArrayTests.cs index 3202bd0ba9..07f10330f3 100644 --- a/test/Npgsql.Tests/Types/ArrayTests.cs +++ b/test/Npgsql.Tests/Types/ArrayTests.cs @@ -177,7 +177,7 @@ public async Task Generic_IList() var reader = await cmd.ExecuteReaderAsync(); reader.Read(); - Assert.AreEqual(expected, reader.GetFieldValue(0)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expected)); } [Test, Description("Verifies that an InvalidOperationException is thrown when the returned array has a different number of dimensions from what was requested.")] @@ -389,9 +389,9 @@ public async Task Read_two_empty_arrays() await using var cmd = new NpgsqlCommand("SELECT '{}'::INT[], '{}'::INT[]", conn); await using var reader = await cmd.ExecuteReaderAsync(); await reader.ReadAsync(); - Assert.AreSame(reader.GetFieldValue(0), reader.GetFieldValue(1)); + Assert.That(reader.GetFieldValue(1), Is.SameAs(reader.GetFieldValue(0))); // Unlike T[], List is mutable so we should not return the same instance - Assert.AreNotSame(reader.GetFieldValue>(0), reader.GetFieldValue>(1)); + Assert.That(reader.GetFieldValue>(1), Is.Not.SameAs(reader.GetFieldValue>(0))); } [Test] diff --git a/test/Npgsql.Tests/Types/ByteaTests.cs b/test/Npgsql.Tests/Types/ByteaTests.cs index 60cc2830f3..9e8df154b0 100644 --- a/test/Npgsql.Tests/Types/ByteaTests.cs +++ b/test/Npgsql.Tests/Types/ByteaTests.cs @@ -279,9 +279,9 @@ public async Task Array_of_bytea() var inVal = new[] { bytes, bytes }; cmd.Parameters.AddWithValue("p1", NpgsqlDbType.Bytea | NpgsqlDbType.Array, inVal); var retVal = (byte[][]?)await cmd.ExecuteScalarAsync(); - Assert.AreEqual(inVal.Length, retVal!.Length); - Assert.AreEqual(inVal[0], retVal[0]); - Assert.AreEqual(inVal[1], retVal[1]); + Assert.That(retVal!.Length, Is.EqualTo(inVal.Length)); + Assert.That(retVal[0], Is.EqualTo(inVal[0])); + Assert.That(retVal[1], Is.EqualTo(inVal[1])); } sealed class NonSeekableStream(byte[] data) : MemoryStream(data) diff --git a/test/Npgsql.Tests/Types/CompositeHandlerTests.Read.cs b/test/Npgsql.Tests/Types/CompositeHandlerTests.Read.cs index 2188569a49..732dbf83e1 100644 --- a/test/Npgsql.Tests/Types/CompositeHandlerTests.Read.cs +++ b/test/Npgsql.Tests/Types/CompositeHandlerTests.Read.cs @@ -32,27 +32,27 @@ async Task Read(T composite, Action, T> assert, string? schema = null [Test] public Task Read_class_with_property() => - Read((execute, expected) => Assert.AreEqual(expected.Value, execute().Value)); + Read((execute, expected) => Assert.That(execute().Value, Is.EqualTo(expected.Value))); [Test] public Task Read_class_with_field() => - Read((execute, expected) => Assert.AreEqual(expected.Value, execute().Value)); + Read((execute, expected) => Assert.That(execute().Value, Is.EqualTo(expected.Value))); [Test] public Task Read_struct_with_property() => - Read((execute, expected) => Assert.AreEqual(expected.Value, execute().Value)); + Read((execute, expected) => Assert.That(execute().Value, Is.EqualTo(expected.Value))); [Test] public Task Read_struct_with_field() => - Read((execute, expected) => Assert.AreEqual(expected.Value, execute().Value)); + Read((execute, expected) => Assert.That(execute().Value, Is.EqualTo(expected.Value))); [Test] public Task Read_type_with_two_properties() => Read((execute, expected) => { var actual = execute(); - Assert.AreEqual(expected.IntValue, actual.IntValue); - Assert.AreEqual(expected.StringValue, actual.StringValue); + Assert.That(actual.IntValue, Is.EqualTo(expected.IntValue)); + Assert.That(actual.StringValue, Is.EqualTo(expected.StringValue)); }); [Test] @@ -60,8 +60,8 @@ public Task Read_type_with_two_properties_inverted() => Read((execute, expected) => { var actual = execute(); - Assert.AreEqual(expected.IntValue, actual.IntValue); - Assert.AreEqual(expected.StringValue, actual.StringValue); + Assert.That(actual.IntValue, Is.EqualTo(expected.IntValue)); + Assert.That(actual.StringValue, Is.EqualTo(expected.StringValue)); }); [Test] @@ -98,7 +98,7 @@ public Task Read_type_with_more_properties_than_attributes() => Read(new TypeWithMorePropertiesThanAttributes(), (execute, expected) => { var actual = execute(); - Assert.That(actual.IntValue, Is.Not.Null); + Assert.That((int?)actual.IntValue, Is.Not.Null); Assert.That(actual.StringValue, Is.Null); }); diff --git a/test/Npgsql.Tests/Types/CompositeHandlerTests.Write.cs b/test/Npgsql.Tests/Types/CompositeHandlerTests.Write.cs index 160b037a97..800270f7c3 100644 --- a/test/Npgsql.Tests/Types/CompositeHandlerTests.Write.cs +++ b/test/Npgsql.Tests/Types/CompositeHandlerTests.Write.cs @@ -45,34 +45,34 @@ async Task Write(T composite, Action? assert = null, str [Test] public Task Write_class_with_property() - => Write((reader, expected) => Assert.AreEqual(expected.Value, reader.GetString(0))); + => Write((reader, expected) => Assert.That(reader.GetString(0), Is.EqualTo(expected.Value))); [Test] public Task Write_class_with_field() - => Write((reader, expected) => Assert.AreEqual(expected.Value, reader.GetString(0))); + => Write((reader, expected) => Assert.That(reader.GetString(0), Is.EqualTo(expected.Value))); [Test] public Task Write_struct_with_property() - => Write((reader, expected) => Assert.AreEqual(expected.Value, reader.GetString(0))); + => Write((reader, expected) => Assert.That(reader.GetString(0), Is.EqualTo(expected.Value))); [Test] public Task Write_struct_with_field() - => Write((reader, expected) => Assert.AreEqual(expected.Value, reader.GetString(0))); + => Write((reader, expected) => Assert.That(reader.GetString(0), Is.EqualTo(expected.Value))); [Test] public Task Write_type_with_two_properties() => Write((reader, expected) => { - Assert.AreEqual(expected.IntValue, reader.GetInt32(0)); - Assert.AreEqual(expected.StringValue, reader.GetString(1)); + Assert.That(reader.GetInt32(0), Is.EqualTo(expected.IntValue)); + Assert.That(reader.GetString(1), Is.EqualTo(expected.StringValue)); }); [Test] public Task Write_type_with_two_properties_inverted() => Write((reader, expected) => { - Assert.AreEqual(expected.IntValue, reader.GetInt32(1)); - Assert.AreEqual(expected.StringValue, reader.GetString(0)); + Assert.That(reader.GetInt32(1), Is.EqualTo(expected.IntValue)); + Assert.That(reader.GetString(0), Is.EqualTo(expected.StringValue)); }); [Test] diff --git a/test/Npgsql.Tests/Types/CompositeTests.cs b/test/Npgsql.Tests/Types/CompositeTests.cs index 765508908c..1c1b254862 100644 --- a/test/Npgsql.Tests/Types/CompositeTests.cs +++ b/test/Npgsql.Tests/Types/CompositeTests.cs @@ -453,8 +453,8 @@ public async Task Table_as_composite([Values] bool enabled) Assert.ThrowsAsync(DoAssertion); // Start a transaction specifically for multiplexing (to bind a connector to the connection) await using var tx = await connection.BeginTransactionAsync(); - Assert.Null(connection.Connector!.DatabaseInfo.CompositeTypes.SingleOrDefault(c => c.Name.Contains(table))); - Assert.Null(connection.Connector!.DatabaseInfo.ArrayTypes.SingleOrDefault(c => c.Name.Contains(table))); + Assert.That(connection.Connector!.DatabaseInfo.CompositeTypes.SingleOrDefault(c => c.Name.Contains(table)), Is.Null); + Assert.That(connection.Connector!.DatabaseInfo.ArrayTypes.SingleOrDefault(c => c.Name.Contains(table)), Is.Null); } diff --git a/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index 078693bf96..fe7bb1bd27 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -442,8 +442,8 @@ public void NpgsqlParameterDbType_is_value_dependent_datetime_or_datetime2() { var localtimestamp = new NpgsqlParameter { Value = DateTime.Now }; var unspecifiedtimestamp = new NpgsqlParameter { Value = new DateTime() }; - Assert.AreEqual(DbType.DateTime2, localtimestamp.DbType); - Assert.AreEqual(DbType.DateTime2, unspecifiedtimestamp.DbType); + Assert.That(localtimestamp.DbType, Is.EqualTo(DbType.DateTime2)); + Assert.That(unspecifiedtimestamp.DbType, Is.EqualTo(DbType.DateTime2)); // We don't support any DateTimeOffset other than offset 0 which maps to timestamptz, // we might add an exception for offset == DateTimeOffset.Now.Offset (local offset) mapping to timestamp at some point. @@ -452,8 +452,8 @@ public void NpgsqlParameterDbType_is_value_dependent_datetime_or_datetime2() var timestamptz = new NpgsqlParameter { Value = DateTime.UtcNow }; var dtotimestamptz = new NpgsqlParameter { Value = DateTimeOffset.UtcNow }; - Assert.AreEqual(DbType.DateTime, timestamptz.DbType); - Assert.AreEqual(DbType.DateTime, dtotimestamptz.DbType); + Assert.That(timestamptz.DbType, Is.EqualTo(DbType.DateTime)); + Assert.That(dtotimestamptz.DbType, Is.EqualTo(DbType.DateTime)); } [Test] @@ -461,13 +461,13 @@ public void NpgsqlParameterNpgsqlDbType_is_value_dependent_timestamp_or_timestam { var localtimestamp = new NpgsqlParameter { Value = DateTime.Now }; var unspecifiedtimestamp = new NpgsqlParameter { Value = new DateTime() }; - Assert.AreEqual(NpgsqlDbType.Timestamp, localtimestamp.NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.Timestamp, unspecifiedtimestamp.NpgsqlDbType); + Assert.That(localtimestamp.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); + Assert.That(unspecifiedtimestamp.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); var timestamptz = new NpgsqlParameter { Value = DateTime.UtcNow }; var dtotimestamptz = new NpgsqlParameter { Value = DateTimeOffset.UtcNow }; - Assert.AreEqual(NpgsqlDbType.TimestampTz, timestamptz.NpgsqlDbType); - Assert.AreEqual(NpgsqlDbType.TimestampTz, dtotimestamptz.NpgsqlDbType); + Assert.That(timestamptz.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz)); + Assert.That(dtotimestamptz.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz)); } [Test] diff --git a/test/Npgsql.Tests/Types/EnumTests.cs b/test/Npgsql.Tests/Types/EnumTests.cs index b6e56c632b..7161e6408c 100644 --- a/test/Npgsql.Tests/Types/EnumTests.cs +++ b/test/Npgsql.Tests/Types/EnumTests.cs @@ -42,7 +42,7 @@ public async Task Data_source_unmap() var isUnmapSuccessful = dataSourceBuilder.UnmapEnum(type); await using var dataSource = dataSourceBuilder.Build(); - Assert.IsTrue(isUnmapSuccessful); + Assert.That(isUnmapSuccessful); Assert.ThrowsAsync(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null)); } @@ -72,7 +72,7 @@ public async Task Data_source_unmap_non_generic() var isUnmapSuccessful = dataSourceBuilder.UnmapEnum(typeof(Mood), type); await using var dataSource = dataSourceBuilder.Build(); - Assert.IsTrue(isUnmapSuccessful); + Assert.That(isUnmapSuccessful); Assert.ThrowsAsync(() => AssertType(dataSource, Mood.Happy, "happy", type, npgsqlDbType: null)); } @@ -170,11 +170,11 @@ public async Task Unmapped_enum_as_clr_enum_supported_only_with_EnableUnmappedTy nameof(NpgsqlDataSourceBuilder)); var exception = await AssertTypeUnsupportedWrite(Mood.Happy, enumType); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead("happy", enumType); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } diff --git a/test/Npgsql.Tests/Types/FullTextSearchTests.cs b/test/Npgsql.Tests/Types/FullTextSearchTests.cs index 9c5104051a..bccefdef3f 100644 --- a/test/Npgsql.Tests/Types/FullTextSearchTests.cs +++ b/test/Npgsql.Tests/Types/FullTextSearchTests.cs @@ -67,20 +67,20 @@ public async Task Full_text_search_not_supported_by_default_on_NpgsqlSlimSourceB await using var dataSource = dataSourceBuilder.Build(); var exception = await AssertTypeUnsupportedRead("a", "tsquery", dataSource); - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedWrite(new NpgsqlTsQueryLexeme("a"), pgTypeName: null, dataSource); - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead("1", "tsvector", dataSource); - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedWrite(NpgsqlTsVector.Parse("'1'"), pgTypeName: null, dataSource); - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } [Test] diff --git a/test/Npgsql.Tests/Types/InternalTypeTests.cs b/test/Npgsql.Tests/Types/InternalTypeTests.cs index 9c68a66695..cd1c2190ed 100644 --- a/test/Npgsql.Tests/Types/InternalTypeTests.cs +++ b/test/Npgsql.Tests/Types/InternalTypeTests.cs @@ -52,10 +52,10 @@ public async Task Tid() cmd.Parameters.AddWithValue("p", NpgsqlDbType.Tid, expected); using var reader = await cmd.ExecuteReaderAsync(); reader.Read(); - Assert.AreEqual(1234, reader.GetFieldValue(0).BlockNumber); - Assert.AreEqual(40000, reader.GetFieldValue(0).OffsetNumber); - Assert.AreEqual(expected.BlockNumber, reader.GetFieldValue(1).BlockNumber); - Assert.AreEqual(expected.OffsetNumber, reader.GetFieldValue(1).OffsetNumber); + Assert.That(reader.GetFieldValue(0).BlockNumber, Is.EqualTo(1234)); + Assert.That(reader.GetFieldValue(0).OffsetNumber, Is.EqualTo(40000)); + Assert.That(reader.GetFieldValue(1).BlockNumber, Is.EqualTo(expected.BlockNumber)); + Assert.That(reader.GetFieldValue(1).OffsetNumber, Is.EqualTo(expected.OffsetNumber)); } #region NpgsqlLogSequenceNumber / PgLsn @@ -78,7 +78,7 @@ public bool NpgsqlLogSequenceNumber_equals(NpgsqlLogSequenceNumber lsn, object? public async Task NpgsqlLogSequenceNumber() { var expected1 = new NpgsqlLogSequenceNumber(42949672971ul); - Assert.AreEqual(expected1, NpgsqlTypes.NpgsqlLogSequenceNumber.Parse("A/B")); + Assert.That(NpgsqlTypes.NpgsqlLogSequenceNumber.Parse("A/B"), Is.EqualTo(expected1)); await using var conn = await OpenConnectionAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT 'A/B'::pg_lsn, @p::pg_lsn"; @@ -87,12 +87,12 @@ public async Task NpgsqlLogSequenceNumber() reader.Read(); var result1 = reader.GetFieldValue(0); var result2 = reader.GetFieldValue(1); - Assert.AreEqual(expected1, result1); - Assert.AreEqual(42949672971ul, (ulong)result1); - Assert.AreEqual("A/B", result1.ToString()); - Assert.AreEqual(expected1, result2); - Assert.AreEqual(42949672971ul, (ulong)result2); - Assert.AreEqual("A/B", result2.ToString()); + Assert.That(result1, Is.EqualTo(expected1)); + Assert.That((ulong)result1, Is.EqualTo(42949672971ul)); + Assert.That(result1.ToString(), Is.EqualTo("A/B")); + Assert.That(result2, Is.EqualTo(expected1)); + Assert.That((ulong)result2, Is.EqualTo(42949672971ul)); + Assert.That(result2.ToString(), Is.EqualTo("A/B")); } #endregion NpgsqlLogSequenceNumber / PgLsn diff --git a/test/Npgsql.Tests/Types/JsonDynamicTests.cs b/test/Npgsql.Tests/Types/JsonDynamicTests.cs index 21ff2700da..59a3d24662 100644 --- a/test/Npgsql.Tests/Types/JsonDynamicTests.cs +++ b/test/Npgsql.Tests/Types/JsonDynamicTests.cs @@ -132,7 +132,7 @@ public async Task As_poco_supported_only_with_EnableDynamicJson() PostgresType, base.DataSource); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead( @@ -142,7 +142,7 @@ public async Task As_poco_supported_only_with_EnableDynamicJson() PostgresType, base.DataSource); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } @@ -520,6 +520,12 @@ public JsonDynamicTests(MultiplexingMode multiplexingMode, NpgsqlDbType npgsqlDb protected override NpgsqlDataSource DataSource { get; } + [OneTimeTearDown] + protected void CleanUpDataSource() + { + DataSource.Dispose(); + } + bool IsJsonb => NpgsqlDbType == NpgsqlDbType.Jsonb; string PostgresType => IsJsonb ? "jsonb" : "json"; readonly NpgsqlDbType NpgsqlDbType; diff --git a/test/Npgsql.Tests/Types/JsonPathTests.cs b/test/Npgsql.Tests/Types/JsonPathTests.cs index 022c8eaaf9..ebd4a468c1 100644 --- a/test/Npgsql.Tests/Types/JsonPathTests.cs +++ b/test/Npgsql.Tests/Types/JsonPathTests.cs @@ -51,6 +51,6 @@ public async Task Write(string query, string expected) using var cmd = new NpgsqlCommand($"SELECT 'Passed' WHERE @p::text = {query}::text", conn) { Parameters = { new NpgsqlParameter("p", NpgsqlDbType.JsonPath) { Value = expected } } }; using var rdr = await cmd.ExecuteReaderAsync(); - Assert.True(rdr.Read()); + Assert.That(rdr.Read()); } } diff --git a/test/Npgsql.Tests/Types/MultirangeTests.cs b/test/Npgsql.Tests/Types/MultirangeTests.cs index 86bebb1b67..adb56ed2b9 100644 --- a/test/Npgsql.Tests/Types/MultirangeTests.cs +++ b/test/Npgsql.Tests/Types/MultirangeTests.cs @@ -150,18 +150,18 @@ public async Task Unmapped_multirange_supported_only_with_EnableUnmappedTypes() new("moo", "zoo"), }, multirangeTypeName); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead("""{["bar","foo"],["moo","zoo"]}""", multirangeTypeName); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead>( """{["bar","foo"],["moo","zoo"]}""", multirangeTypeName); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } diff --git a/test/Npgsql.Tests/Types/RangeTests.cs b/test/Npgsql.Tests/Types/RangeTests.cs index f0f9fa6637..0b90824d95 100644 --- a/test/Npgsql.Tests/Types/RangeTests.cs +++ b/test/Npgsql.Tests/Types/RangeTests.cs @@ -73,23 +73,23 @@ public void Equality_finite() //different bounds var r2 = new NpgsqlRange(1, true, false, 2, false, false); - Assert.IsFalse(r1 == r2); + Assert.That(r1 == r2, Is.False); //lower bound is not inclusive var r3 = new NpgsqlRange(0, false, false, 1, false, false); - Assert.IsFalse(r1 == r3); + Assert.That(r1 == r3, Is.False); //upper bound is inclusive var r4 = new NpgsqlRange(0, true, false, 1, true, false); - Assert.IsFalse(r1 == r4); + Assert.That(r1 == r4, Is.False); var r5 = new NpgsqlRange(0, true, false, 1, false, false); - Assert.IsTrue(r1 == r5); + Assert.That(r1 == r5); //check some other combinations while we are here - Assert.IsFalse(r2 == r3); - Assert.IsFalse(r2 == r4); - Assert.IsFalse(r3 == r4); + Assert.That(r2 == r3, Is.False); + Assert.That(r2 == r4, Is.False); + Assert.That(r3 == r4, Is.False); } [Test] @@ -99,20 +99,20 @@ public void Equality_infinite() //different upper bound (lower bound shouldn't matter since it is infinite) var r2 = new NpgsqlRange(1, false, true, 2, false, false); - Assert.IsFalse(r1 == r2); + Assert.That(r1 == r2, Is.False); //upper bound is inclusive var r3 = new NpgsqlRange(0, false, true, 1, true, false); - Assert.IsFalse(r1 == r3); + Assert.That(r1 == r3, Is.False); //value of lower bound shouldn't matter since it is infinite var r4 = new NpgsqlRange(10, false, true, 1, false, false); - Assert.IsTrue(r1 == r4); + Assert.That(r1 == r4); //check some other combinations while we are here - Assert.IsFalse(r2 == r3); - Assert.IsFalse(r2 == r4); - Assert.IsFalse(r3 == r4); + Assert.That(r2 == r3, Is.False); + Assert.That(r2 == r4, Is.False); + Assert.That(r3 == r4, Is.False); } [Test] @@ -122,12 +122,12 @@ public void GetHashCode_value_types() NpgsqlRange b = NpgsqlRange.Empty; NpgsqlRange c = NpgsqlRange.Parse("(,)"); - Assert.IsFalse(a.Equals(b)); - Assert.IsFalse(a.Equals(c)); - Assert.IsFalse(b.Equals(c)); - Assert.AreNotEqual(a.GetHashCode(), b.GetHashCode()); - Assert.AreNotEqual(a.GetHashCode(), c.GetHashCode()); - Assert.AreNotEqual(b.GetHashCode(), c.GetHashCode()); + Assert.That(a.Equals(b), Is.False); + Assert.That(a.Equals(c), Is.False); + Assert.That(b.Equals(c), Is.False); + Assert.That(b.GetHashCode(), Is.Not.EqualTo(a.GetHashCode())); + Assert.That(c.GetHashCode(), Is.Not.EqualTo(a.GetHashCode())); + Assert.That(c.GetHashCode(), Is.Not.EqualTo(b.GetHashCode())); } [Test] @@ -137,12 +137,12 @@ public void GetHashCode_reference_types() NpgsqlRange b = NpgsqlRange.Empty; NpgsqlRange c = NpgsqlRange.Parse("(,)"); - Assert.IsFalse(a.Equals(b)); - Assert.IsFalse(a.Equals(c)); - Assert.IsFalse(b.Equals(c)); - Assert.AreNotEqual(a.GetHashCode(), b.GetHashCode()); - Assert.AreNotEqual(a.GetHashCode(), c.GetHashCode()); - Assert.AreNotEqual(b.GetHashCode(), c.GetHashCode()); + Assert.That(a.Equals(b), Is.False); + Assert.That(a.Equals(c), Is.False); + Assert.That(b.Equals(c), Is.False); + Assert.That(b.GetHashCode(), Is.Not.EqualTo(a.GetHashCode())); + Assert.That(c.GetHashCode(), Is.Not.EqualTo(a.GetHashCode())); + Assert.That(c.GetHashCode(), Is.Not.EqualTo(b.GetHashCode())); } [Test] @@ -208,15 +208,15 @@ public async Task Unmapped_range_supported_only_with_EnableUnmappedTypes() nameof(NpgsqlDataSourceBuilder)); var exception = await AssertTypeUnsupportedWrite(new NpgsqlRange("bar", "foo"), rangeType); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead("""["bar","foo"]""", rangeType); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = await AssertTypeUnsupportedRead>("""["bar","foo"]""", rangeType); - Assert.IsInstanceOf(exception.InnerException); + Assert.That(exception.InnerException, Is.InstanceOf()); Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } @@ -288,7 +288,7 @@ public void Roundtrip_DateTime_ranges_through_ToString_and_Parse(NpgsqlRange.Parse(wellKnownText); - Assert.AreEqual(input, result); + Assert.That(result, Is.EqualTo(input)); } [Theory] @@ -298,7 +298,7 @@ public void Roundtrip_DateTime_ranges_through_ToString_and_Parse(NpgsqlRange.Parse(value); - Assert.AreEqual(NpgsqlRange.Empty, result); + Assert.That(result, Is.EqualTo(NpgsqlRange.Empty)); } [Theory] @@ -310,7 +310,7 @@ public void Parse_empty(string value) public void Roundtrip_int_ranges_through_ToString_and_Parse(string input) { var result = NpgsqlRange.Parse(input); - Assert.AreEqual(input.Replace(" ", null), result.ToString()); + Assert.That(result.ToString(), Is.EqualTo(input.Replace(" ", null))); } [Theory] @@ -330,7 +330,7 @@ public void Roundtrip_int_ranges_through_ToString_and_Parse(string input) public void Int_range_Parse_ToString_returns_normalized_representations(string input, string normalized) { var result = NpgsqlRange.Parse(input); - Assert.AreEqual(normalized, result.ToString()); + Assert.That(result.ToString(), Is.EqualTo(normalized)); } [Theory] @@ -350,7 +350,7 @@ public void Int_range_Parse_ToString_returns_normalized_representations(string i public void Nullable_int_range_Parse_ToString_returns_normalized_representations(string input, string normalized) { var result = NpgsqlRange.Parse(input); - Assert.AreEqual(normalized, result.ToString()); + Assert.That(result.ToString(), Is.EqualTo(normalized)); } [Theory] @@ -361,7 +361,7 @@ public void Nullable_int_range_Parse_ToString_returns_normalized_representations public void String_range_Parse_ToString_returns_normalized_representations(string input, string normalized) { var result = NpgsqlRange.Parse(input); - Assert.AreEqual(normalized, result.ToString()); + Assert.That(result.ToString(), Is.EqualTo(normalized)); } [Theory] @@ -369,7 +369,7 @@ public void String_range_Parse_ToString_returns_normalized_representations(strin public void Roundtrip_string_ranges_through_ToString_and_Parse2(string input) { var result = NpgsqlRange.Parse(input); - Assert.AreEqual(input, result.ToString()); + Assert.That(result.ToString(), Is.EqualTo(input)); } [Theory] @@ -388,12 +388,12 @@ public void TypeConverter() var converter = TypeDescriptor.GetConverter(typeof(NpgsqlRange)); // Act - Assert.IsInstanceOf.RangeTypeConverter>(converter); - Assert.IsTrue(converter.CanConvertFrom(typeof(string))); + Assert.That(converter, Is.InstanceOf.RangeTypeConverter>()); + Assert.That(converter.CanConvertFrom(typeof(string))); var result = converter.ConvertFromString("empty"); // Assert - Assert.AreEqual(NpgsqlRange.Empty, result); + Assert.That(result, Is.Empty); } #endregion diff --git a/test/Npgsql.Tests/Types/RecordTests.cs b/test/Npgsql.Tests/Types/RecordTests.cs index 0823323041..86c3fd1875 100644 --- a/test/Npgsql.Tests/Types/RecordTests.cs +++ b/test/Npgsql.Tests/Types/RecordTests.cs @@ -103,8 +103,8 @@ public async Task As_ValueTuple_supported_only_with_EnableRecordsAsTuples() nameof(NpgsqlSlimDataSourceBuilder.EnableRecords)); var exception = Assert.Throws(() => reader.GetFieldValue<(int, string)>(0))!; - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } [Test] @@ -127,12 +127,12 @@ public async Task Records_not_supported_by_default_on_NpgsqlSlimSourceBuilder() nameof(NpgsqlSlimDataSourceBuilder.EnableRecords)); var exception = Assert.Throws(() => reader.GetValue(0))!; - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); exception = Assert.Throws(() => reader.GetFieldValue(0))!; - Assert.IsInstanceOf(exception.InnerException); - Assert.AreEqual(errorMessage, exception.InnerException!.Message); + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); } [Test] diff --git a/test/Npgsql.Tests/Types/TextTests.cs b/test/Npgsql.Tests/Types/TextTests.cs index f7e770c088..13dd94861b 100644 --- a/test/Npgsql.Tests/Types/TextTests.cs +++ b/test/Npgsql.Tests/Types/TextTests.cs @@ -138,15 +138,15 @@ public async Task Internal_char() var expected = new char[] { 'a', (char)(256 - 3), 'b', (char)66, (char)230 }; for (var i = 0; i < expected.Length; i++) { - Assert.AreEqual(expected[i], reader.GetChar(i)); + Assert.That(reader.GetChar(i), Is.EqualTo(expected[i])); } var arr = (char[])reader.GetValue(5); var arr2 = (char[])reader.GetValue(6); - Assert.AreEqual(testArr.Length, arr.Length); + Assert.That(arr.Length, Is.EqualTo(testArr.Length)); for (var i = 0; i < arr.Length; i++) { - Assert.AreEqual(testArr[i], arr[i]); - Assert.AreEqual(testArr2[i], arr2[i]); + Assert.That(arr[i], Is.EqualTo(testArr[i])); + Assert.That(arr2[i], Is.EqualTo(testArr2[i])); } } } diff --git a/test/Npgsql.Tests/TypesTests.cs b/test/Npgsql.Tests/TypesTests.cs index 1f6b0e8c55..4110a0856f 100644 --- a/test/Npgsql.Tests/TypesTests.cs +++ b/test/Npgsql.Tests/TypesTests.cs @@ -17,22 +17,22 @@ public void TsVector() NpgsqlTsVector vec; vec = NpgsqlTsVector.Parse("a"); - Assert.AreEqual("'a'", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo("'a'")); vec = NpgsqlTsVector.Parse("a "); - Assert.AreEqual("'a'", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo("'a'")); vec = NpgsqlTsVector.Parse("a:1A"); - Assert.AreEqual("'a':1A", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo("'a':1A")); vec = NpgsqlTsVector.Parse(@"\abc\def:1a "); - Assert.AreEqual("'abcdef':1A", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo("'abcdef':1A")); vec = NpgsqlTsVector.Parse(@"abc:3A 'abc' abc:4B 'hello''yo' 'meh\'\\':5"); - Assert.AreEqual(@"'abc':3A,4B 'hello''yo' 'meh''\\':5", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo(@"'abc':3A,4B 'hello''yo' 'meh''\\':5")); vec = NpgsqlTsVector.Parse(" a:12345C a:24D a:25B b c d 1 2 a:25A,26B,27,28"); - Assert.AreEqual("'1' '2' 'a':24,25A,26B,27,28,12345C 'b' 'c' 'd'", vec.ToString()); + Assert.That(vec.ToString(), Is.EqualTo("'1' '2' 'a':24,25A,26B,27,28,12345C 'b' 'c' 'd'")); } [Test] @@ -47,27 +47,27 @@ public void TsQuery() var str = query.ToString(); query = NpgsqlTsQuery.Parse("a & b | c"); - Assert.AreEqual("'a' & 'b' | 'c'", query.ToString()); + Assert.That(query.ToString(), Is.EqualTo("'a' & 'b' | 'c'")); query = NpgsqlTsQuery.Parse("'a''':*ab&d:d&!c"); - Assert.AreEqual("'a''':*AB & 'd':D & !'c'", query.ToString()); + Assert.That(query.ToString(), Is.EqualTo("'a''':*AB & 'd':D & !'c'")); query = NpgsqlTsQuery.Parse("(a & !(c | d)) & (!!a&b) | c | d | e"); - Assert.AreEqual("( ( 'a' & !( 'c' | 'd' ) & !( !'a' ) & 'b' | 'c' ) | 'd' ) | 'e'", query.ToString()); - Assert.AreEqual(query.ToString(), NpgsqlTsQuery.Parse(query.ToString()).ToString()); + Assert.That(query.ToString(), Is.EqualTo("( ( 'a' & !( 'c' | 'd' ) & !( !'a' ) & 'b' | 'c' ) | 'd' ) | 'e'")); + Assert.That(NpgsqlTsQuery.Parse(query.ToString()).ToString(), Is.EqualTo(query.ToString())); query = NpgsqlTsQuery.Parse("(((a:*)))"); - Assert.AreEqual("'a':*", query.ToString()); + Assert.That(query.ToString(), Is.EqualTo("'a':*")); query = NpgsqlTsQuery.Parse(@"'a\\b''cde'"); - Assert.AreEqual(@"a\b'cde", ((NpgsqlTsQueryLexeme)query).Text); - Assert.AreEqual(@"'a\\b''cde'", query.ToString()); + Assert.That(((NpgsqlTsQueryLexeme)query).Text, Is.EqualTo(@"a\b'cde")); + Assert.That(query.ToString(), Is.EqualTo(@"'a\\b''cde'")); query = NpgsqlTsQuery.Parse(@"a <-> b"); - Assert.AreEqual("'a' <-> 'b'", query.ToString()); + Assert.That(query.ToString(), Is.EqualTo("'a' <-> 'b'")); query = NpgsqlTsQuery.Parse("((a & b) <5> c) <-> !d <0> e"); - Assert.AreEqual("( ( 'a' & 'b' <5> 'c' ) <-> !'d' ) <0> 'e'", query.ToString()); + Assert.That(query.ToString(), Is.EqualTo("( ( 'a' & 'b' <5> 'c' ) <-> !'d' ) <0> 'e'")); Assert.Throws(typeof(FormatException), () => NpgsqlTsQuery.Parse("a b c & &")); Assert.Throws(typeof(FormatException), () => NpgsqlTsQuery.Parse("&")); @@ -89,8 +89,8 @@ public void TsQuery() [Test] public void TsVector_empty() { - Assert.IsEmpty(NpgsqlTsVector.Empty); - Assert.AreEqual(string.Empty, NpgsqlTsVector.Empty.ToString()); + Assert.That(NpgsqlTsVector.Empty, Is.Empty); + Assert.That(NpgsqlTsVector.Empty.ToString(), Is.Empty); } [Test] @@ -167,18 +167,18 @@ public void TsQueryEquatibility() void AreEqual(NpgsqlTsQuery left, NpgsqlTsQuery right) { - Assert.True(left == right); - Assert.False(left != right); - Assert.AreEqual(left, right); - Assert.AreEqual(left.GetHashCode(), right.GetHashCode()); + Assert.That(left == right); + Assert.That(left != right, Is.False); + Assert.That(right, Is.EqualTo(left)); + Assert.That(right.GetHashCode(), Is.EqualTo(left.GetHashCode())); } void AreNotEqual(NpgsqlTsQuery left, NpgsqlTsQuery right) { - Assert.False(left == right); - Assert.True(left != right); - Assert.AreNotEqual(left, right); - Assert.AreNotEqual(left.GetHashCode(), right.GetHashCode()); + Assert.That(left == right, Is.False); + Assert.That(left != right); + Assert.That(right, Is.Not.EqualTo(left)); + Assert.That(right.GetHashCode(), Is.Not.EqualTo(left.GetHashCode())); } } @@ -188,7 +188,7 @@ public void TsQueryOperatorPrecedence() { var query = NpgsqlTsQuery.Parse("!a <-> b & c | d & e"); var expectedGrouping = NpgsqlTsQuery.Parse("((!(a) <-> b) & c) | (d & e)"); - Assert.AreEqual(expectedGrouping.ToString(), query.ToString()); + Assert.That(query.ToString(), Is.EqualTo(expectedGrouping.ToString())); } #pragma warning restore CS0618 // {NpgsqlTsVector,NpgsqlTsQuery}.Parse are obsolete @@ -204,14 +204,14 @@ public void NpgsqlPolygon_empty() public void NpgsqlPath_default() { NpgsqlPath defaultPath = default; - Assert.IsFalse(defaultPath.Equals([new(1, 2)])); + Assert.That(defaultPath.Equals([new(1, 2)]), Is.False); } [Test] public void NpgsqlPolygon_default() { NpgsqlPolygon defaultPolygon = default; - Assert.IsFalse(defaultPolygon.Equals([new(1, 2)])); + Assert.That(defaultPolygon.Equals([new(1, 2)]), Is.False); } [Test] From c62a9be02d10c2cade268eb74283b583c7dfa313 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 10 Sep 2025 12:13:11 +0200 Subject: [PATCH 056/155] Bump extension versions to 10.0.0-rc.1 (#6209) --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d4671ecb06..bc9d30b8bb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - - + + @@ -26,8 +26,8 @@ - - + + @@ -35,7 +35,7 @@ - + From ba77cf8dbf41bfe21db2ea80aec1e03d22a179c1 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 10 Sep 2025 13:17:33 +0300 Subject: [PATCH 057/155] Fix logical replication tests with PostgreSQL 18 (#6171) PostgreSQL 18 doesn't send messages for aborted transactions --- .../Replication/PgOutputReplicationTests.cs | 129 +++++++++++------- 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs index e3cffd5766..3bbcd1b6ac 100644 --- a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs @@ -10,6 +10,7 @@ using Npgsql.Replication; using Npgsql.Replication.PgOutput; using Npgsql.Replication.PgOutput.Messages; +using Npgsql.Util; using TruncateOptions = Npgsql.Replication.PgOutput.Messages.TruncateMessage.TruncateOptions; using ReplicaIdentitySetting = Npgsql.Replication.PgOutput.Messages.RelationMessage.ReplicaIdentitySetting; using static Npgsql.Tests.TestUtil; @@ -728,77 +729,99 @@ public Task LogicalDecodingMessage(bool writeMessages, bool readMessages) } } - if (IsStreaming) + // PostgreSQL 18 skips logical decoding of already-aborted transactions + if (c.PostgreSqlVersion.IsGreaterOrEqual(18)) { - // Begin Transaction 2 - transactionXid = await AssertTransactionStart(messages); - - // Relation - await NextMessage(messages); - - // Inserts - for (var insertCount = 0; insertCount < 10; insertCount++) - await NextMessage(messages); - - // LogicalDecodingMessage 2 (transactional) + // LogicalDecodingMessage 2 (non-transactional) if (writeMessages) { var msg = await NextMessage(messages); - Assert.That(msg.TransactionXid, IsStreaming ? Is.EqualTo(transactionXid) : Is.Null); - Assert.That(msg.Flags, Is.EqualTo(1)); + Assert.That(msg.TransactionXid, Is.Null); + Assert.That(msg.Flags, Is.EqualTo(0)); Assert.That(msg.Prefix, Is.EqualTo(prefix)); - Assert.That(msg.Data.Length, Is.EqualTo(transactionalMessage.Length)); + Assert.That(msg.Data.Length, Is.EqualTo(nonTransactionalMessage.Length)); if (readMessages) { var buffer = new MemoryStream(); await msg.Data.CopyToAsync(buffer, CancellationToken.None); - Assert.That(rc.Encoding.GetString(buffer.ToArray()), Is.EqualTo(transactionalMessage)); + Assert.That(rc.Encoding.GetString(buffer.ToArray()), Is.EqualTo(nonTransactionalMessage)); } } - - // Further inserts - // We don't try to predict how many insert messages we get here - // since the streaming transaction will most likely abort before - // we reach the expected number - while (await messages.MoveNextAsync() && messages.Current is InsertMessage - || messages.Current is StreamStopMessage - && await messages.MoveNextAsync() - && messages.Current is StreamStartMessage - && await messages.MoveNextAsync() - && messages.Current is InsertMessage) + } + else + { + if (IsStreaming) { - // Ignore + // Begin Transaction 2 + transactionXid = await AssertTransactionStart(messages); + + // Relation + await NextMessage(messages); + + // Inserts + for (var insertCount = 0; insertCount < 10; insertCount++) + await NextMessage(messages); + + // LogicalDecodingMessage 2 (transactional) + if (writeMessages) + { + var msg = await NextMessage(messages); + Assert.That(msg.TransactionXid, IsStreaming ? Is.EqualTo(transactionXid) : Is.Null); + Assert.That(msg.Flags, Is.EqualTo(1)); + Assert.That(msg.Prefix, Is.EqualTo(prefix)); + Assert.That(msg.Data.Length, Is.EqualTo(transactionalMessage.Length)); + if (readMessages) + { + var buffer = new MemoryStream(); + await msg.Data.CopyToAsync(buffer, CancellationToken.None); + Assert.That(rc.Encoding.GetString(buffer.ToArray()), Is.EqualTo(transactionalMessage)); + } + } + + // Further inserts + // We don't try to predict how many insert messages we get here + // since the streaming transaction will most likely abort before + // we reach the expected number + while (await messages.MoveNextAsync() && messages.Current is InsertMessage + || messages.Current is StreamStopMessage + && await messages.MoveNextAsync() + && messages.Current is StreamStartMessage + && await messages.MoveNextAsync() + && messages.Current is InsertMessage) + { + // Ignore + } } - } - else if (writeMessages) - await messages.MoveNextAsync(); + else if (writeMessages) + await messages.MoveNextAsync(); - // LogicalDecodingMessage 3 (non-transactional) - if (writeMessages) - { - var msg = (LogicalDecodingMessage)messages.Current; - Assert.That(msg.TransactionXid, Is.Null); - Assert.That(msg.Flags, Is.EqualTo(0)); - Assert.That(msg.Prefix, Is.EqualTo(prefix)); - Assert.That(msg.Data.Length, Is.EqualTo(nonTransactionalMessage.Length)); - if (readMessages) + // LogicalDecodingMessage 3 (non-transactional) + if (writeMessages) { - var buffer = new MemoryStream(); - await msg.Data.CopyToAsync(buffer, CancellationToken.None); - Assert.That(rc.Encoding.GetString(buffer.ToArray()), Is.EqualTo(nonTransactionalMessage)); + var msg = (LogicalDecodingMessage)messages.Current; + Assert.That(msg.TransactionXid, Is.Null); + Assert.That(msg.Flags, Is.EqualTo(0)); + Assert.That(msg.Prefix, Is.EqualTo(prefix)); + Assert.That(msg.Data.Length, Is.EqualTo(nonTransactionalMessage.Length)); + if (readMessages) + { + var buffer = new MemoryStream(); + await msg.Data.CopyToAsync(buffer, CancellationToken.None); + Assert.That(rc.Encoding.GetString(buffer.ToArray()), Is.EqualTo(nonTransactionalMessage)); + } + + if (IsStreaming) + await messages.MoveNextAsync(); } + // Rollback Transaction 2 if (IsStreaming) - await messages.MoveNextAsync(); - } - - // Rollback Transaction 2 - if (IsStreaming) - { - Assert.That(messages.Current, - _streamingMode == PgOutputStreamingMode.On - ? Is.TypeOf() - : Is.TypeOf()); + { + Assert.That(messages.Current, + _streamingMode == PgOutputStreamingMode.On + ? Is.TypeOf() + : Is.TypeOf()); + } } streamingCts.Cancel(); From 8f9711249bd6fcafbea350aad4a1cdca448b3ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:36:38 +0200 Subject: [PATCH 058/155] Update various dependencies (#6188) --- Directory.Packages.props | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bc9d30b8bb..acb8a524c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,17 +2,12 @@ - - - - - - + - + - + @@ -21,7 +16,7 @@ - + @@ -29,10 +24,10 @@ - + - - + + @@ -40,9 +35,9 @@ - + - + From 7bbf43a67958dc735ac532cf2c9a62d15a927b3c Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 10 Sep 2025 14:32:51 +0300 Subject: [PATCH 059/155] Fix possible deadlock while asynchronously reading values from reader (#6202) Fixes #6190 --- .../Internal/Converters/AsyncHelpers.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Npgsql/Internal/Converters/AsyncHelpers.cs b/src/Npgsql/Internal/Converters/AsyncHelpers.cs index ddd03a24be..bf85a06a9f 100644 --- a/src/Npgsql/Internal/Converters/AsyncHelpers.cs +++ b/src/Npgsql/Internal/Converters/AsyncHelpers.cs @@ -37,9 +37,17 @@ public abstract class CompletionSource public sealed class CompletionSource : CompletionSource { - AsyncValueTaskMethodBuilder _amb = AsyncValueTaskMethodBuilder.Create(); + AsyncValueTaskMethodBuilder _amb; - public ValueTask Task => _amb.Task; + public ValueTask Task { get; } + + public CompletionSource() + { + _amb = AsyncValueTaskMethodBuilder.Create(); + // AsyncValueTaskMethodBuilder's Task and SetResult aren't thread safe in regard to each other + // Which is why we access it prematurely + Task = _amb.Task; + } public void SetResult(T value) => _amb.SetResult(value); @@ -50,9 +58,17 @@ public override void SetException(Exception exception) public sealed class PoolingCompletionSource : CompletionSource { - PoolingAsyncValueTaskMethodBuilder _amb = PoolingAsyncValueTaskMethodBuilder.Create(); + PoolingAsyncValueTaskMethodBuilder _amb; - public ValueTask Task => _amb.Task; + public ValueTask Task { get; } + + public PoolingCompletionSource() + { + _amb = PoolingAsyncValueTaskMethodBuilder.Create(); + // PoolingAsyncValueTaskMethodBuilder's Task and SetResult aren't thread safe in regard to each other + // Which is why we access it prematurely + Task = _amb.Task; + } public void SetResult(T value) => _amb.SetResult(value); @@ -90,7 +106,7 @@ public CompletionSourceContinuation(object handle, delegate*(); OnCompletedWithSource(task.AsTask(), source, new(instance, &UnboxAndComplete)); return source.Task; @@ -111,7 +127,7 @@ public static unsafe ValueTask ReadAsObjectAsyncAsT(this PgConverter in if (task.IsCompletedSuccessfully) return new((T)task.Result); - // Otherwise we do one additional allocation, this allow us to share state machine codegen for all Ts. + // Otherwise we do one additional allocation, this allows us to share state machine codegen for all Ts. var source = new PoolingCompletionSource(); OnCompletedWithSource(task.AsTask(), source, new(instance, &UnboxAndComplete)); return source.Task; From 398d65a4e2e4c2c52eb79be1fcc6fb2110cc5cca Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 10 Sep 2025 16:59:17 +0200 Subject: [PATCH 060/155] Revert "Move to PublicApiAnalyzers v4 (#6185)" This reverts commit ffc3fba1b4f611390b2b23c4ca78d86de89bd272. --- Directory.Packages.props | 2 +- src/Npgsql/PublicAPI.Shipped.txt | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index acb8a524c7..ea2e4ebb3e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + diff --git a/src/Npgsql/PublicAPI.Shipped.txt b/src/Npgsql/PublicAPI.Shipped.txt index 246a515cc2..3ec604ddc0 100644 --- a/src/Npgsql/PublicAPI.Shipped.txt +++ b/src/Npgsql/PublicAPI.Shipped.txt @@ -1228,8 +1228,6 @@ NpgsqlTypes.NpgsqlBox.Width.get -> double NpgsqlTypes.NpgsqlCidr NpgsqlTypes.NpgsqlCidr.Address.get -> System.Net.IPAddress! NpgsqlTypes.NpgsqlCidr.Deconstruct(out System.Net.IPAddress! address, out byte netmask) -> void -NpgsqlTypes.NpgsqlCidr.Equals(NpgsqlTypes.NpgsqlCidr other) -> bool -NpgsqlTypes.NpgsqlInet.Equals(NpgsqlTypes.NpgsqlInet other) -> bool NpgsqlTypes.NpgsqlCidr.Netmask.get -> byte NpgsqlTypes.NpgsqlCidr.NpgsqlCidr() -> void NpgsqlTypes.NpgsqlCidr.NpgsqlCidr(string! addr) -> void @@ -1795,12 +1793,10 @@ override Npgsql.Schema.NpgsqlDbColumn.this[string! propertyName].get -> object? override NpgsqlTypes.NpgsqlBox.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlBox.GetHashCode() -> int override NpgsqlTypes.NpgsqlBox.ToString() -> string! -override NpgsqlTypes.NpgsqlCidr.GetHashCode() -> int override NpgsqlTypes.NpgsqlCidr.ToString() -> string! override NpgsqlTypes.NpgsqlCircle.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlCircle.GetHashCode() -> int override NpgsqlTypes.NpgsqlCircle.ToString() -> string! -override NpgsqlTypes.NpgsqlInet.GetHashCode() -> int override NpgsqlTypes.NpgsqlInet.ToString() -> string! override NpgsqlTypes.NpgsqlInterval.Equals(object? obj) -> bool override NpgsqlTypes.NpgsqlInterval.GetHashCode() -> int @@ -1890,14 +1886,10 @@ static NpgsqlTypes.NpgsqlBox.operator !=(NpgsqlTypes.NpgsqlBox x, NpgsqlTypes.Np static NpgsqlTypes.NpgsqlBox.operator ==(NpgsqlTypes.NpgsqlBox x, NpgsqlTypes.NpgsqlBox y) -> bool static NpgsqlTypes.NpgsqlCidr.explicit operator System.Net.IPAddress!(NpgsqlTypes.NpgsqlCidr cidr) -> System.Net.IPAddress! static NpgsqlTypes.NpgsqlCidr.implicit operator NpgsqlTypes.NpgsqlInet(NpgsqlTypes.NpgsqlCidr cidr) -> NpgsqlTypes.NpgsqlInet -static NpgsqlTypes.NpgsqlCidr.operator !=(NpgsqlTypes.NpgsqlCidr left, NpgsqlTypes.NpgsqlCidr right) -> bool -static NpgsqlTypes.NpgsqlCidr.operator ==(NpgsqlTypes.NpgsqlCidr left, NpgsqlTypes.NpgsqlCidr right) -> bool static NpgsqlTypes.NpgsqlCircle.operator !=(NpgsqlTypes.NpgsqlCircle x, NpgsqlTypes.NpgsqlCircle y) -> bool static NpgsqlTypes.NpgsqlCircle.operator ==(NpgsqlTypes.NpgsqlCircle x, NpgsqlTypes.NpgsqlCircle y) -> bool static NpgsqlTypes.NpgsqlInet.explicit operator System.Net.IPAddress!(NpgsqlTypes.NpgsqlInet inet) -> System.Net.IPAddress! static NpgsqlTypes.NpgsqlInet.implicit operator NpgsqlTypes.NpgsqlInet(System.Net.IPAddress! ip) -> NpgsqlTypes.NpgsqlInet -static NpgsqlTypes.NpgsqlInet.operator !=(NpgsqlTypes.NpgsqlInet left, NpgsqlTypes.NpgsqlInet right) -> bool -static NpgsqlTypes.NpgsqlInet.operator ==(NpgsqlTypes.NpgsqlInet left, NpgsqlTypes.NpgsqlInet right) -> bool static NpgsqlTypes.NpgsqlLine.operator !=(NpgsqlTypes.NpgsqlLine x, NpgsqlTypes.NpgsqlLine y) -> bool static NpgsqlTypes.NpgsqlLine.operator ==(NpgsqlTypes.NpgsqlLine x, NpgsqlTypes.NpgsqlLine y) -> bool static NpgsqlTypes.NpgsqlLogSequenceNumber.explicit operator NpgsqlTypes.NpgsqlLogSequenceNumber(ulong value) -> NpgsqlTypes.NpgsqlLogSequenceNumber @@ -1942,13 +1934,5 @@ static NpgsqlTypes.NpgsqlTsVector.Parse(string! value) -> NpgsqlTypes.NpgsqlTsVe static readonly Npgsql.NpgsqlFactory.Instance -> Npgsql.NpgsqlFactory! static readonly NpgsqlTypes.NpgsqlLogSequenceNumber.Invalid -> NpgsqlTypes.NpgsqlLogSequenceNumber static readonly NpgsqlTypes.NpgsqlRange.Empty -> NpgsqlTypes.NpgsqlRange -virtual Npgsql.NoticeEventHandler.Invoke(object! sender, Npgsql.NpgsqlNoticeEventArgs! e) -> void -virtual Npgsql.NotificationEventHandler.Invoke(object! sender, Npgsql.NpgsqlNotificationEventArgs! e) -> void virtual Npgsql.NpgsqlCommand.Clone() -> Npgsql.NpgsqlCommand! -virtual Npgsql.NpgsqlRowUpdatedEventHandler.Invoke(object! sender, Npgsql.NpgsqlRowUpdatedEventArgs! e) -> void -virtual Npgsql.NpgsqlRowUpdatingEventHandler.Invoke(object! sender, Npgsql.NpgsqlRowUpdatingEventArgs! e) -> void -virtual Npgsql.ProvideClientCertificatesCallback.Invoke(System.Security.Cryptography.X509Certificates.X509CertificateCollection! certificates) -> void -virtual Npgsql.ProvidePasswordCallback.Invoke(string! host, int port, string! database, string! username) -> string! virtual Npgsql.Replication.PgOutput.ReplicationTuple.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerator! -~override NpgsqlTypes.NpgsqlCidr.Equals(object obj) -> bool -~override NpgsqlTypes.NpgsqlInet.Equals(object obj) -> bool From d3314594f48c3722f185842b7ea6f32a3da6503f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 08:32:31 +0200 Subject: [PATCH 061/155] Bump BenchmarkDotNet from 0.15.2 to 0.15.3 (#6215) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ea2e4ebb3e..49c6e2c5f6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,7 @@ - + From d03b78d12c4be25bafa9e456a549b4dd2435d1c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:46:44 +0200 Subject: [PATCH 062/155] Bump BenchmarkDotNet from 0.15.3 to 0.15.4 (#6221) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 49c6e2c5f6..9f5ae3fd0b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,7 @@ - + From ee978428cfa1bcfb85f6569c1601a8ab66fff789 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 1 Oct 2025 17:51:51 +0300 Subject: [PATCH 063/155] Suppress ExecutionContext while creating timers (#6106) Fixes #6105 --- src/Npgsql/Internal/NpgsqlConnector.cs | 5 ++++- src/Npgsql/NpgsqlDataSource.cs | 5 +++-- src/Npgsql/PoolingDataSource.cs | 3 ++- src/Npgsql/Replication/ReplicationConnection.cs | 7 +++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 44b9504c71..8018abe56d 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -400,7 +400,10 @@ internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn) _isKeepAliveEnabled = Settings.KeepAlive > 0; if (_isKeepAliveEnabled) - _keepAliveTimer = new Timer(PerformKeepAlive, null, Timeout.Infinite, Timeout.Infinite); + { + using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever + _keepAliveTimer = new Timer(PerformKeepAlive, null, Timeout.Infinite, Timeout.Infinite); + } DataReader = new NpgsqlDataReader(this); diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index d8ff956141..cc177602db 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -124,8 +124,9 @@ internal NpgsqlDataSource( _timerPasswordProviderCancellationTokenSource = new(); - // Create the timer, but don't start it; the manual run below will will schedule the first refresh. - _periodicPasswordProviderTimer = new Timer(state => _ = RefreshPassword(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + // Create the timer, but don't start it; the manual run below will schedule the first refresh. + using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever + _periodicPasswordProviderTimer = new Timer(state => _ = RefreshPassword(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); // Trigger the first refresh attempt right now, outside the timer; this allows us to capture the Task so it can be observed // in GetPasswordAsync. _passwordRefreshTask = Task.Run(RefreshPassword); diff --git a/src/Npgsql/PoolingDataSource.cs b/src/Npgsql/PoolingDataSource.cs index c339047fb4..a6c63494a8 100644 --- a/src/Npgsql/PoolingDataSource.cs +++ b/src/Npgsql/PoolingDataSource.cs @@ -97,7 +97,8 @@ internal PoolingDataSource( if (connectionIdleLifetime < pruningSamplingInterval) throw new ArgumentException($"Connection can't have {nameof(settings.ConnectionIdleLifetime)} {connectionIdleLifetime} under {nameof(settings.ConnectionPruningInterval)} {pruningSamplingInterval}"); - _pruningTimer = new Timer(PruningTimerCallback, this, Timeout.Infinite, Timeout.Infinite); + using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever + _pruningTimer = new Timer(PruningTimerCallback, this, Timeout.Infinite, Timeout.Infinite); _pruningSampleSize = DivideRoundingUp(settings.ConnectionIdleLifetime, settings.ConnectionPruningInterval); _pruningMedianIndex = DivideRoundingUp(_pruningSampleSize, 2) - 1; // - 1 to go from length to index _pruningSamplingInterval = pruningSamplingInterval; diff --git a/src/Npgsql/Replication/ReplicationConnection.cs b/src/Npgsql/Replication/ReplicationConnection.cs index 4a41467164..8583c31ce0 100644 --- a/src/Npgsql/Replication/ReplicationConnection.cs +++ b/src/Npgsql/Replication/ReplicationConnection.cs @@ -452,8 +452,11 @@ internal async IAsyncEnumerator StartReplicationInternal( SetTimeouts(_walReceiverTimeout, CommandTimeout); - _sendFeedbackTimer = new Timer(TimerSendFeedback, state: null, WalReceiverStatusInterval, Timeout.InfiniteTimeSpan); - _requestFeedbackTimer = new Timer(TimerRequestFeedback, state: null, _requestFeedbackInterval, Timeout.InfiniteTimeSpan); + using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever + { + _sendFeedbackTimer = new Timer(TimerSendFeedback, state: null, WalReceiverStatusInterval, Timeout.InfiniteTimeSpan); + _requestFeedbackTimer = new Timer(TimerRequestFeedback, state: null, _requestFeedbackInterval, Timeout.InfiniteTimeSpan); + } while (true) { From 444c77aea97e5273e4d938b7023f4e25a5f120b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:26:35 +0200 Subject: [PATCH 064/155] Rewrite NpgsqlConnectionStringBuilderSourceGenerator as incremental (#6186) And bump Microsoft.CodeAnalysis.Analyzers and Microsoft.CodeAnalysis.CSharp --- Directory.Packages.props | 4 +- ...lConnectionStringBuilderSourceGenerator.cs | 161 +++++++++--------- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9f5ae3fd0b..9e51fdc983 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,8 +14,8 @@ - - + + diff --git a/src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilderSourceGenerator.cs b/src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilderSourceGenerator.cs index 665789e74e..c7c7228321 100644 --- a/src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilderSourceGenerator.cs +++ b/src/Npgsql.SourceGenerators/NpgsqlConnectionStringBuilderSourceGenerator.cs @@ -9,7 +9,7 @@ namespace Npgsql.SourceGenerators; [Generator] -public class NpgsqlConnectionStringBuilderSourceGenerator : ISourceGenerator +public class NpgsqlConnectionStringBuilderSourceGenerator : IIncrementalGenerator { static readonly DiagnosticDescriptor InternalError = new DiagnosticDescriptor( id: "PGXXXX", @@ -19,106 +19,107 @@ public class NpgsqlConnectionStringBuilderSourceGenerator : ISourceGenerator DiagnosticSeverity.Error, isEnabledByDefault: true); - public void Initialize(GeneratorInitializationContext context) {} - - public void Execute(GeneratorExecutionContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - if (context.Compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringBuilder") is not { } type) - return; - - if (context.Compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringPropertyAttribute") is not - { } connectionStringPropertyAttribute) - { - context.ReportDiagnostic(Diagnostic.Create( - InternalError, - location: null, - "Could not find Npgsql.NpgsqlConnectionStringPropertyAttribute")); - return; - } - - var obsoleteAttribute = context.Compilation.GetTypeByMetadataName("System.ObsoleteAttribute"); - var displayNameAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DisplayNameAttribute"); - var defaultValueAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DefaultValueAttribute"); - - if (obsoleteAttribute is null || displayNameAttribute is null || defaultValueAttribute is null) + var compilationProvider = context.CompilationProvider; + context.RegisterSourceOutput(compilationProvider, (spc, compilation) => { - context.ReportDiagnostic(Diagnostic.Create( - InternalError, - location: null, - "Could not find ObsoleteAttribute, DisplayNameAttribute or DefaultValueAttribute")); - return; - } - - var properties = new List(); - var propertiesByKeyword = new Dictionary(); - foreach (var member in type.GetMembers()) - { - if (member is not IPropertySymbol property || - property.GetAttributes().FirstOrDefault(a => connectionStringPropertyAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) is not { } propertyAttribute || - property.GetAttributes() - .FirstOrDefault(a => displayNameAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) - ?.ConstructorArguments[0].Value is not string displayName) + var type = compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringBuilder"); + if (type is null) + return; + + var connectionStringPropertyAttribute = compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringPropertyAttribute"); + if (connectionStringPropertyAttribute is null) { - continue; + spc.ReportDiagnostic(Diagnostic.Create( + InternalError, + location: null, + "Could not find Npgsql.NpgsqlConnectionStringPropertyAttribute")); + return; } - var explicitDefaultValue = property.GetAttributes() - .FirstOrDefault(a => defaultValueAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) - ?.ConstructorArguments[0].Value; - - if (explicitDefaultValue is string s) - explicitDefaultValue = '"' + s.Replace("\"", "\"\"") + '"'; + var obsoleteAttribute = compilation.GetTypeByMetadataName("System.ObsoleteAttribute"); + var displayNameAttribute = compilation.GetTypeByMetadataName("System.ComponentModel.DisplayNameAttribute"); + var defaultValueAttribute = compilation.GetTypeByMetadataName("System.ComponentModel.DefaultValueAttribute"); - if (explicitDefaultValue is not null && property.Type.TypeKind == TypeKind.Enum) + if (obsoleteAttribute is null || displayNameAttribute is null || defaultValueAttribute is null) { - explicitDefaultValue = $"({property.Type.Name}){explicitDefaultValue}"; - // var foo = property.Type.Name; - // explicitDefaultValue += $"/* {foo} */"; + spc.ReportDiagnostic(Diagnostic.Create( + InternalError, + location: null, + "Could not find ObsoleteAttribute, DisplayNameAttribute or DefaultValueAttribute")); + return; } - var propertyDetails = new PropertyDetails + var properties = new List(); + var propertiesByKeyword = new Dictionary(); + foreach (var member in type.GetMembers()) { - Name = property.Name, - CanonicalName = displayName, - TypeName = property.Type.Name, - IsEnum = property.Type.TypeKind == TypeKind.Enum, - IsObsolete = property.GetAttributes().Any(a => obsoleteAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)), - DefaultValue = explicitDefaultValue - }; + if (member is not IPropertySymbol property || + property.GetAttributes().FirstOrDefault(a => connectionStringPropertyAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) is not { } propertyAttribute || + property.GetAttributes() + .FirstOrDefault(a => displayNameAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) + ?.ConstructorArguments[0].Value is not string displayName) + { + continue; + } - properties.Add(propertyDetails); + var explicitDefaultValue = property.GetAttributes() + .FirstOrDefault(a => defaultValueAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) + ?.ConstructorArguments[0].Value; - propertiesByKeyword[displayName.ToUpperInvariant()] = propertyDetails; - if (property.Name != displayName) - { - var propertyName = property.Name.ToUpperInvariant(); - if (!propertiesByKeyword.ContainsKey(propertyName)) - propertyDetails.Alternatives.Add(propertyName); - } + if (explicitDefaultValue is string s) + explicitDefaultValue = '"' + s.Replace("\"", "\"\"") + '"'; - if (propertyAttribute.ConstructorArguments.Length == 1) - { - foreach (var synonymArg in propertyAttribute.ConstructorArguments[0].Values) + if (explicitDefaultValue is not null && property.Type.TypeKind == TypeKind.Enum) { - if (synonymArg.Value is string synonym) + explicitDefaultValue = $"({property.Type.Name}){explicitDefaultValue}"; + } + + var propertyDetails = new PropertyDetails + { + Name = property.Name, + CanonicalName = displayName, + TypeName = property.Type.Name, + IsEnum = property.Type.TypeKind == TypeKind.Enum, + IsObsolete = property.GetAttributes().Any(a => obsoleteAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)), + DefaultValue = explicitDefaultValue + }; + + properties.Add(propertyDetails); + + propertiesByKeyword[displayName.ToUpperInvariant()] = propertyDetails; + if (property.Name != displayName) + { + var propertyName = property.Name.ToUpperInvariant(); + if (!propertiesByKeyword.ContainsKey(propertyName)) + propertyDetails.Alternatives.Add(propertyName); + } + + if (propertyAttribute.ConstructorArguments.Length == 1) + { + foreach (var synonymArg in propertyAttribute.ConstructorArguments[0].Values) { - var synonymName = synonym.ToUpperInvariant(); - if (!propertiesByKeyword.ContainsKey(synonymName)) - propertyDetails.Alternatives.Add(synonymName); + if (synonymArg.Value is string synonym) + { + var synonymName = synonym.ToUpperInvariant(); + if (!propertiesByKeyword.ContainsKey(synonymName)) + propertyDetails.Alternatives.Add(synonymName); + } } } } - } - var template = Template.Parse(EmbeddedResource.GetContent("NpgsqlConnectionStringBuilder.snbtxt"), "NpgsqlConnectionStringBuilder.snbtxt"); + var template = Template.Parse(EmbeddedResource.GetContent("NpgsqlConnectionStringBuilder.snbtxt"), "NpgsqlConnectionStringBuilder.snbtxt"); - var output = template.Render(new - { - Properties = properties, - PropertiesByKeyword = propertiesByKeyword - }); + var output = template.Render(new + { + Properties = properties, + PropertiesByKeyword = propertiesByKeyword + }); - context.AddSource(type.Name + ".Generated.cs", SourceText.From(output, Encoding.UTF8)); + spc.AddSource(type.Name + ".Generated.cs", SourceText.From(output, Encoding.UTF8)); + }); } sealed class PropertyDetails From d58efec0c236b7e9763a8ce09b1308521f2850a5 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 2 Oct 2025 14:15:05 +0300 Subject: [PATCH 065/155] Add COPY operations dispose on initialization failure (#6220) Fixes #6219 --- src/Npgsql/NpgsqlBinaryExporter.cs | 29 ++++++++---- src/Npgsql/NpgsqlBinaryImporter.cs | 6 ++- src/Npgsql/NpgsqlConnection.cs | 75 ++++++++++++++++++++++++------ src/Npgsql/NpgsqlRawCopyStream.cs | 35 ++++++++++---- 4 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/Npgsql/NpgsqlBinaryExporter.cs b/src/Npgsql/NpgsqlBinaryExporter.cs index f221056119..9473c95959 100644 --- a/src/Npgsql/NpgsqlBinaryExporter.cs +++ b/src/Npgsql/NpgsqlBinaryExporter.cs @@ -24,7 +24,7 @@ public sealed class NpgsqlBinaryExporter : ICancelable NpgsqlConnector _connector; NpgsqlReadBuffer _buf; - bool _isConsumed, _isDisposed; + ExporterState _state = ExporterState.Uninitialized; long _endOfMessagePos; short _column; @@ -91,6 +91,7 @@ internal async Task Init(string copyToCommand, bool async, CancellationToken can throw _connector.UnexpectedMessageReceived(msg.Code); } + _state = ExporterState.Ready; NumColumns = copyOutResponse.NumColumns; _columnInfoCache = new PgConverterInfo[NumColumns]; _rowsExported = 0; @@ -141,7 +142,7 @@ async Task ReadHeader(bool async) async ValueTask StartRow(bool async, CancellationToken cancellationToken = default) { ThrowIfDisposed(); - if (_isConsumed) + if (_state == ExporterState.Consumed) return -1; using var registration = _connector.StartNestedCancellableOperation(cancellationToken); @@ -176,7 +177,7 @@ async ValueTask StartRow(bool async, CancellationToken cancellationToken = Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); _column = BeforeRow; - _isConsumed = true; + _state = ExporterState.Consumed; return -1; } @@ -437,7 +438,7 @@ void ThrowIfNotOnRow() void ThrowIfDisposed() { - if (_isDisposed) + if (_state == ExporterState.Disposed) ThrowHelper.ThrowObjectDisposedException(nameof(NpgsqlBinaryExporter), "The COPY operation has already ended."); } @@ -472,10 +473,10 @@ public Task CancelAsync() async ValueTask DisposeAsync(bool async) { - if (_isDisposed) + if (_state == ExporterState.Disposed) return; - if (_isConsumed) + if (_state is ExporterState.Consumed or ExporterState.Uninitialized) { LogMessages.BinaryCopyOperationCompleted(_copyLogger, _rowsExported, _connector.Id); } @@ -512,7 +513,7 @@ async ValueTask DisposeAsync(bool async) void Cleanup() { - Debug.Assert(!_isDisposed); + Debug.Assert(_state != ExporterState.Disposed); var connector = _connector; if (!ReferenceEquals(connector, null)) @@ -523,9 +524,21 @@ void Cleanup() } _buf = null!; - _isDisposed = true; + _state = ExporterState.Disposed; } } #endregion + + #region Enums + + enum ExporterState + { + Uninitialized, + Ready, + Consumed, + Disposed + } + + #endregion Enums } diff --git a/src/Npgsql/NpgsqlBinaryImporter.cs b/src/Npgsql/NpgsqlBinaryImporter.cs index 633d1bac15..52c9438fde 100644 --- a/src/Npgsql/NpgsqlBinaryImporter.cs +++ b/src/Npgsql/NpgsqlBinaryImporter.cs @@ -25,7 +25,7 @@ public sealed class NpgsqlBinaryImporter : ICancelable NpgsqlConnector _connector; NpgsqlWriteBuffer _buf; - ImporterState _state; + ImporterState _state = ImporterState.Uninitialized; /// /// The number of columns in the current (not-yet-written) row. @@ -99,6 +99,7 @@ internal async Task Init(string copyFromCommand, bool async, CancellationToken c throw _connector.UnexpectedMessageReceived(msg.Code); } + _state = ImporterState.Ready; _params = new NpgsqlParameter[copyInResponse.NumColumns]; _rowsImported = 0; _buf.StartCopyMode(); @@ -512,6 +513,7 @@ async ValueTask CloseAsync(bool async, CancellationToken cancellationToken = def case ImporterState.Ready: await Cancel(async, cancellationToken).ConfigureAwait(false); break; + case ImporterState.Uninitialized: case ImporterState.Cancelled: case ImporterState.Committed: break; @@ -553,6 +555,7 @@ void CheckReady() static void Throw(ImporterState state) => throw (state switch { + ImporterState.Uninitialized => throw new InvalidOperationException("The COPY operation has not been initialized."), ImporterState.Disposed => new ObjectDisposedException(typeof(NpgsqlBinaryImporter).FullName, "The COPY operation has already ended."), ImporterState.Cancelled => new InvalidOperationException("The COPY operation has already been cancelled."), @@ -567,6 +570,7 @@ static void Throw(ImporterState state) enum ImporterState { + Uninitialized, Ready, Committed, Cancelled, diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index c63f5bc4b6..6bda4f71d5 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -1160,17 +1160,26 @@ async Task BeginBinaryImport(bool async, string copyFromCo LogMessages.StartingBinaryImport(connector.LoggingConfiguration.CopyLogger, connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); + var importer = new NpgsqlBinaryImporter(connector); try { - var importer = new NpgsqlBinaryImporter(connector); await importer.Init(copyFromCommand, async, cancellationToken).ConfigureAwait(false); connector.CurrentCopyOperation = importer; return importer; } catch { - connector.EndUserAction(); - EndBindingScope(ConnectorBindingScope.Copy); + try + { + if (async) + await importer.DisposeAsync().ConfigureAwait(false); + else + importer.Dispose(); + } + catch + { + // ignored + } throw; } } @@ -1210,17 +1219,26 @@ async Task BeginBinaryExport(bool async, string copyToComm LogMessages.StartingBinaryExport(connector.LoggingConfiguration.CopyLogger, connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); + var exporter = new NpgsqlBinaryExporter(connector); try { - var exporter = new NpgsqlBinaryExporter(connector); await exporter.Init(copyToCommand, async, cancellationToken).ConfigureAwait(false); connector.CurrentCopyOperation = exporter; return exporter; } catch { - connector.EndUserAction(); - EndBindingScope(ConnectorBindingScope.Copy); + try + { + if (async) + await exporter.DisposeAsync().ConfigureAwait(false); + else + exporter.Dispose(); + } + catch + { + // ignored + } throw; } } @@ -1266,9 +1284,9 @@ async Task BeginTextImport(bool async, string copyFromCommand, Cance LogMessages.StartingTextImport(connector.LoggingConfiguration.CopyLogger, connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); + var copyStream = new NpgsqlRawCopyStream(connector); try { - var copyStream = new NpgsqlRawCopyStream(connector); await copyStream.Init(copyFromCommand, async, cancellationToken).ConfigureAwait(false); var writer = new NpgsqlCopyTextWriter(connector, copyStream); connector.CurrentCopyOperation = writer; @@ -1276,8 +1294,17 @@ async Task BeginTextImport(bool async, string copyFromCommand, Cance } catch { - connector.EndUserAction(); - EndBindingScope(ConnectorBindingScope.Copy); + try + { + if (async) + await copyStream.DisposeAsync().ConfigureAwait(false); + else + copyStream.Dispose(); + } + catch + { + // ignored + } throw; } } @@ -1323,9 +1350,9 @@ async Task BeginTextExport(bool async, string copyToCommand, Cancell LogMessages.StartingTextExport(connector.LoggingConfiguration.CopyLogger, connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); + var copyStream = new NpgsqlRawCopyStream(connector); try { - var copyStream = new NpgsqlRawCopyStream(connector); await copyStream.Init(copyToCommand, async, cancellationToken).ConfigureAwait(false); var reader = new NpgsqlCopyTextReader(connector, copyStream); connector.CurrentCopyOperation = reader; @@ -1333,8 +1360,17 @@ async Task BeginTextExport(bool async, string copyToCommand, Cancell } catch { - connector.EndUserAction(); - EndBindingScope(ConnectorBindingScope.Copy); + try + { + if (async) + await copyStream.DisposeAsync().ConfigureAwait(false); + else + copyStream.Dispose(); + } + catch + { + // ignored + } throw; } } @@ -1380,9 +1416,9 @@ async Task BeginRawBinaryCopy(bool async, string copyComman LogMessages.StartingRawCopy(connector.LoggingConfiguration.CopyLogger, connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); + var stream = new NpgsqlRawCopyStream(connector); try { - var stream = new NpgsqlRawCopyStream(connector); await stream.Init(copyCommand, async, cancellationToken).ConfigureAwait(false); if (!stream.IsBinary) { @@ -1395,8 +1431,17 @@ async Task BeginRawBinaryCopy(bool async, string copyComman } catch { - connector.EndUserAction(); - EndBindingScope(ConnectorBindingScope.Copy); + try + { + if (async) + await stream.DisposeAsync().ConfigureAwait(false); + else + stream.Dispose(); + } + catch + { + // ignored + } throw; } } diff --git a/src/Npgsql/NpgsqlRawCopyStream.cs b/src/Npgsql/NpgsqlRawCopyStream.cs index 3648b24075..664c39a1b8 100644 --- a/src/Npgsql/NpgsqlRawCopyStream.cs +++ b/src/Npgsql/NpgsqlRawCopyStream.cs @@ -28,7 +28,7 @@ public sealed class NpgsqlRawCopyStream : Stream, ICancelable NpgsqlWriteBuffer _writeBuf; int _leftToReadInDataMsg; - bool _isDisposed, _isConsumed; + CopyStreamState _state = CopyStreamState.Uninitialized; bool _canRead; bool _canWrite; @@ -84,12 +84,14 @@ internal async Task Init(string copyCommand, bool async, CancellationToken cance switch (msg.Code) { case BackendMessageCode.CopyInResponse: + _state = CopyStreamState.Ready; var copyInResponse = (CopyInResponseMessage) msg; IsBinary = copyInResponse.IsBinary; _canWrite = true; _writeBuf.StartCopyMode(); break; case BackendMessageCode.CopyOutResponse: + _state = CopyStreamState.Ready; var copyOutResponse = (CopyOutResponseMessage) msg; IsBinary = copyOutResponse.IsBinary; _canRead = true; @@ -245,7 +247,7 @@ async ValueTask ReadAsyncInternal() async ValueTask ReadCore(int count, bool async, CancellationToken cancellationToken = default) { - if (_isConsumed) + if (_state == CopyStreamState.Consumed) return 0; using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); @@ -261,7 +263,7 @@ async ValueTask ReadCore(int count, bool async, CancellationToken cancellat } catch { - if (!_isDisposed) + if (_state != CopyStreamState.Disposed) Cleanup(); throw; } @@ -274,7 +276,7 @@ async ValueTask ReadCore(int count, bool async, CancellationToken cancellat case BackendMessageCode.CopyDone: Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); - _isConsumed = true; + _state = CopyStreamState.Consumed; return 0; default: throw _connector.UnexpectedMessageReceived(msg.Code); @@ -331,6 +333,9 @@ async Task Cancel(bool async) } catch (PostgresException e) { + // TODO: NpgsqlBinaryImporter doesn't cleanup on cancellation + // And instead relies on users disposing the object + // We probably should do the same here Cleanup(); if (e.SqlState != PostgresErrorCodes.QueryCanceled) @@ -355,7 +360,7 @@ public override ValueTask DisposeAsync() async ValueTask DisposeAsync(bool disposing, bool async) { - if (_isDisposed || !disposing) + if (_state == CopyStreamState.Disposed || !disposing) return; try @@ -373,7 +378,7 @@ async ValueTask DisposeAsync(bool disposing, bool async) } else { - if (!_isConsumed) + if (_state != CopyStreamState.Consumed && _state != CopyStreamState.Uninitialized) { try { @@ -403,7 +408,7 @@ async ValueTask DisposeAsync(bool disposing, bool async) #pragma warning disable CS8625 void Cleanup() { - Debug.Assert(!_isDisposed); + Debug.Assert(_state != CopyStreamState.Disposed); LogMessages.CopyOperationCompleted(_copyLogger, _connector.Id); _connector.EndUserAction(); _connector.CurrentCopyOperation = null; @@ -411,13 +416,13 @@ void Cleanup() _connector = null; _readBuf = null; _writeBuf = null; - _isDisposed = true; + _state = CopyStreamState.Disposed; } #pragma warning restore CS8625 void CheckDisposed() { - if (_isDisposed) { + if (_state == CopyStreamState.Disposed) { throw new ObjectDisposedException(nameof(NpgsqlRawCopyStream), "The COPY operation has already ended."); } } @@ -452,6 +457,18 @@ static void ValidateArguments(byte[] buffer, int offset, int count) ThrowHelper.ThrowArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); } #endregion + + #region Enums + + enum CopyStreamState + { + Uninitialized, + Ready, + Consumed, + Disposed + } + + #endregion Enums } /// From c9f5866979208c4c999215fcb3cf147145b995cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:04:39 +0200 Subject: [PATCH 066/155] Bump OpenTelemetry.Api from 1.12.0 to 1.13.0 (#6229) --- updated-dependencies: - dependency-nme: OpenTelemetry.Api dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9e51fdc983..52977fb7e1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + From 266e121738b846b8776873904b79832fce1a2651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:05:13 +0200 Subject: [PATCH 067/155] Bump Newtonsoft.Json from 13.0.3 to 13.0.4 (#6228) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 52977fb7e1..4968435b70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + From c4d4ac60e292e318227d421ae08d979f0f4f4930 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:05:39 +0200 Subject: [PATCH 068/155] Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.0 (#6227) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4968435b70..adfbc4adcf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + From 4631da46702817020fe353972ffdcb07cbed1caf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:18:38 +0200 Subject: [PATCH 069/155] Bump Microsoft.CodeAnalysis.CSharp from 4.13.0 to 4.14.0 (#6226) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index adfbc4adcf..9a6a2e0e7d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + From b9bb342c0494c1b00d2a6d59b7c6525bc3c4415f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:49:50 +0200 Subject: [PATCH 070/155] Bump Scriban.Signed from 6.2.1 to 6.4.0 (#6230) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a6a2e0e7d..aee0fe3c5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + From 96a86f142ac6f5d6ad95f935ea315eaa291763d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:50:12 +0200 Subject: [PATCH 071/155] Bump xunit.runner.visualstudio from 3.1.4 to 3.1.5 (#6231) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index aee0fe3c5f..2f6c36102a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + From 6e308c393a10198ca0d55a25bb181ec788087750 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 5 Oct 2025 20:57:33 +0300 Subject: [PATCH 072/155] Allow specifying TargetSessionAttributes in connection string with NpgsqlDataSourceBuilder (#6046) --- src/Npgsql/MultiHostDataSourceWrapper.cs | 16 +-- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 2 +- src/Npgsql/NpgsqlMultiHostDataSource.cs | 2 +- src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 2 - .../Properties/NpgsqlStrings.Designer.cs | 9 -- src/Npgsql/Properties/NpgsqlStrings.resx | 3 - test/Npgsql.Tests/MultipleHostsTests.cs | 110 ++++++++++++++++-- 7 files changed, 109 insertions(+), 35 deletions(-) diff --git a/src/Npgsql/MultiHostDataSourceWrapper.cs b/src/Npgsql/MultiHostDataSourceWrapper.cs index 3217ec95cf..b6b7d3e5f5 100644 --- a/src/Npgsql/MultiHostDataSourceWrapper.cs +++ b/src/Npgsql/MultiHostDataSourceWrapper.cs @@ -10,9 +10,11 @@ namespace Npgsql; sealed class MultiHostDataSourceWrapper(NpgsqlMultiHostDataSource wrappedSource, TargetSessionAttributes targetSessionAttributes) : NpgsqlDataSource(CloneSettingsForTargetSessionAttributes(wrappedSource.Settings, targetSessionAttributes), wrappedSource.Configuration) { + internal NpgsqlMultiHostDataSource WrappedSource { get; } = wrappedSource; + internal override bool OwnsConnectors => false; - public override void Clear() => wrappedSource.Clear(); + public override void Clear() => WrappedSource.Clear(); static NpgsqlConnectionStringBuilder CloneSettingsForTargetSessionAttributes( NpgsqlConnectionStringBuilder settings, @@ -23,22 +25,22 @@ static NpgsqlConnectionStringBuilder CloneSettingsForTargetSessionAttributes( return clonedSettings; } - internal override (int Total, int Idle, int Busy) Statistics => wrappedSource.Statistics; + internal override (int Total, int Idle, int Busy) Statistics => WrappedSource.Statistics; internal override ValueTask Get(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) - => wrappedSource.Get(conn, timeout, async, cancellationToken); + => WrappedSource.Get(conn, timeout, async, cancellationToken); internal override bool TryGetIdleConnector([NotNullWhen(true)] out NpgsqlConnector? connector) => throw new NpgsqlException("Npgsql bug: trying to get an idle connector from " + nameof(MultiHostDataSourceWrapper)); internal override ValueTask OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) => throw new NpgsqlException("Npgsql bug: trying to open a new connector from " + nameof(MultiHostDataSourceWrapper)); internal override void Return(NpgsqlConnector connector) - => wrappedSource.Return(connector); + => WrappedSource.Return(connector); internal override void AddPendingEnlistedConnector(NpgsqlConnector connector, Transaction transaction) - => wrappedSource.AddPendingEnlistedConnector(connector, transaction); + => WrappedSource.AddPendingEnlistedConnector(connector, transaction); internal override bool TryRemovePendingEnlistedConnector(NpgsqlConnector connector, Transaction transaction) - => wrappedSource.TryRemovePendingEnlistedConnector(connector, transaction); + => WrappedSource.TryRemovePendingEnlistedConnector(connector, transaction); internal override bool TryRentEnlistedPending(Transaction transaction, NpgsqlConnection connection, [NotNullWhen(true)] out NpgsqlConnector? connector) - => wrappedSource.TryRentEnlistedPending(transaction, connection, out connector); + => WrappedSource.TryRentEnlistedPending(transaction, connection, out connector); } diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index 35a6ed04e0..7200e45130 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -1001,7 +1001,7 @@ public string? TargetSessionAttributes set { - TargetSessionAttributesParsed = value is null ? null : ParseTargetSessionAttributes(value); + TargetSessionAttributesParsed = value is null ? null : ParseTargetSessionAttributes(value.ToLowerInvariant()); SetValue(nameof(TargetSessionAttributes), value); } } diff --git a/src/Npgsql/NpgsqlMultiHostDataSource.cs b/src/Npgsql/NpgsqlMultiHostDataSource.cs index 7236e7bb8b..4997a6093a 100644 --- a/src/Npgsql/NpgsqlMultiHostDataSource.cs +++ b/src/Npgsql/NpgsqlMultiHostDataSource.cs @@ -456,6 +456,6 @@ bool TryGetValidConnector(List list, TargetSessionAttributes pr static TargetSessionAttributes GetTargetSessionAttributes(NpgsqlConnection connection) => connection.Settings.TargetSessionAttributesParsed ?? (PostgresEnvironment.TargetSessionAttributes is { } s - ? NpgsqlConnectionStringBuilder.ParseTargetSessionAttributes(s) + ? NpgsqlConnectionStringBuilder.ParseTargetSessionAttributes(s.ToLowerInvariant()) : TargetSessionAttributes.Any); } diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 3fa0a5a9fd..4a3d6fdad7 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -855,8 +855,6 @@ _loggerFactory is null void ValidateMultiHost() { - if (ConnectionStringBuilder.TargetSessionAttributes is not null) - throw new InvalidOperationException(NpgsqlStrings.CannotSpecifyTargetSessionAttributes); if (ConnectionStringBuilder.Multiplexing) throw new NotSupportedException("Multiplexing is not supported with multiple hosts"); if (ConnectionStringBuilder.ReplicationMode != ReplicationMode.Off) diff --git a/src/Npgsql/Properties/NpgsqlStrings.Designer.cs b/src/Npgsql/Properties/NpgsqlStrings.Designer.cs index 7f6fca99cd..7f71914ca2 100644 --- a/src/Npgsql/Properties/NpgsqlStrings.Designer.cs +++ b/src/Npgsql/Properties/NpgsqlStrings.Designer.cs @@ -113,15 +113,6 @@ internal static string CannotSetMultiplePasswordProviderKinds { } } - /// - /// Looks up a localized string similar to When creating a multi-host data source, TargetSessionAttributes cannot be specified. Create without TargetSessionAttributes, and then obtain DataSource wrappers from it. Consult the docs for more information.. - /// - internal static string CannotSpecifyTargetSessionAttributes { - get { - return ResourceManager.GetString("CannotSpecifyTargetSessionAttributes", resourceCulture); - } - } - /// /// Looks up a localized string similar to RootCertificate cannot be used in conjunction with SslClientAuthenticationOptionsCallback overwriting RemoteCertificateValidationCallback; when registering a validation callback, perform whatever validation you require in that callback.. /// diff --git a/src/Npgsql/Properties/NpgsqlStrings.resx b/src/Npgsql/Properties/NpgsqlStrings.resx index f523cf6eb2..af951d1a07 100644 --- a/src/Npgsql/Properties/NpgsqlStrings.resx +++ b/src/Npgsql/Properties/NpgsqlStrings.resx @@ -54,9 +54,6 @@ '{0}' must be positive. - - When creating a multi-host data source, TargetSessionAttributes cannot be specified. Create without TargetSessionAttributes, and then obtain DataSource wrappers from it. Consult the docs for more information. - Cannot read interval values with non-zero months as TimeSpan, since that type doesn't support months. Consider using NodaTime Period which better corresponds to PostgreSQL interval, or read the value as NpgsqlInterval, or transform the interval to not contain months or years in PostgreSQL before reading it. diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index f4026cc7f6..66c1a99cf7 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using System.Transactions; -using Npgsql.Properties; using static Npgsql.Tests.Support.MockState; using static Npgsql.Tests.TestUtil; using IsolationLevel = System.Transactions.IsolationLevel; @@ -92,6 +91,55 @@ public async Task Connect_to_correct_host_unpooled(TargetSessionAttributes targe _ = await postmasters[i].WaitForServerConnection(); } + [Test] + [TestCaseSource(nameof(MyCases))] + public async Task Connect_to_correct_host_legacy(TargetSessionAttributes targetSessionAttributes, MockState[] servers, int expectedServer) + { + var postmasters = servers.Select(s => PgPostmasterMock.Start(state: s)).ToArray(); + await using var __ = new DisposableWrapper(postmasters); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = MultipleHosts(postmasters), + ServerCompatibilityMode = ServerCompatibilityMode.NoTypeLoading, + TargetSessionAttributes = TargetSessionAttributesAsString(targetSessionAttributes) + }; + + using var pool = CreateTempPool(connectionStringBuilder, out var connectionString); + await using var conn = new NpgsqlConnection(connectionString); + await conn.OpenAsync(); + + Assert.That(conn.Port, Is.EqualTo(postmasters[expectedServer].Port)); + + for (var i = 0; i <= expectedServer; i++) + _ = await postmasters[i].WaitForServerConnection(); + } + + [Test] + [TestCaseSource(nameof(MyCases))] + public async Task Connect_to_correct_host_connection_string(TargetSessionAttributes targetSessionAttributes, MockState[] servers, int expectedServer) + { + var postmasters = servers.Select(s => PgPostmasterMock.Start(state: s)).ToArray(); + await using var __ = new DisposableWrapper(postmasters); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = MultipleHosts(postmasters), + ServerCompatibilityMode = ServerCompatibilityMode.NoTypeLoading, + TargetSessionAttributes = TargetSessionAttributesAsString(targetSessionAttributes) + }; + + await using var dataSource = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString) + .Build(); + Assert.That(dataSource, Is.TypeOf()); + await using var conn = await dataSource.OpenConnectionAsync(); + + Assert.That(conn.Port, Is.EqualTo(postmasters[expectedServer].Port)); + + for (var i = 0; i <= expectedServer; i++) + _ = await postmasters[i].WaitForServerConnection(); + } + [Test] [TestCaseSource(nameof(MyCases))] public async Task Connect_to_correct_host_with_available_idle( @@ -132,6 +180,40 @@ public async Task Connect_to_correct_host_with_available_idle( _ = await postmasters[i].WaitForServerConnection(); } + [Test] + public async Task Legacy_connection_shares_datasource() + { + await using var primaryPostmaster = PgPostmasterMock.Start(state: Primary); + await using var standbyPostmaster = PgPostmasterMock.Start(state: Standby); + + var builder1 = new NpgsqlConnectionStringBuilder + { + Host = MultipleHosts(primaryPostmaster, standbyPostmaster), + ServerCompatibilityMode = ServerCompatibilityMode.NoTypeLoading, + TargetSessionAttributes = "Prefer-Primary" + }; + + // Use the exact same pool for both connections as CreateTempPool adds a unique `ApplicationName` to connection string + using var pool = CreateTempPool(builder1, out var connectionString1); + var connectionString2 = new NpgsqlConnectionStringBuilder(connectionString1) + { + TargetSessionAttributes = "Prefer-Standby" + }.ConnectionString; + + await using var conn1 = new NpgsqlConnection(connectionString1); + await conn1.OpenAsync(); + Assert.That(conn1.Port, Is.EqualTo(primaryPostmaster.Port)); + + await using var conn2 = new NpgsqlConnection(connectionString2); + await conn2.OpenAsync(); + Assert.That(conn2.Port, Is.EqualTo(standbyPostmaster.Port)); + + Assert.That(conn1.NpgsqlDataSource, Is.Not.SameAs(conn2.NpgsqlDataSource)); + Assert.That(conn1.NpgsqlDataSource, Is.TypeOf()); + Assert.That(conn2.NpgsqlDataSource, Is.TypeOf()); + Assert.That(((MultiHostDataSourceWrapper)conn1.NpgsqlDataSource).WrappedSource, Is.SameAs(((MultiHostDataSourceWrapper)conn2.NpgsqlDataSource).WrappedSource)); + } + [Test] [TestCase(TargetSessionAttributes.Standby, new[] { Primary, Primary })] [TestCase(TargetSessionAttributes.Primary, new[] { Standby, Standby })] @@ -254,7 +336,7 @@ public async Task TargetSessionAttributes_with_single_host(string targetSessionA if (targetSessionAttributes == "any") { - await using var postmasterMock = PgPostmasterMock.Start(ConnectionString); + await using var postmasterMock = PgPostmasterMock.Start(connectionString); using var pool = CreateTempPool(postmasterMock.ConnectionString, out connectionString); await using var conn = new NpgsqlConnection(connectionString); await conn.OpenAsync(); @@ -1023,15 +1105,6 @@ public async Task DataSource_without_wrappers() Assert.That(standbyConnection.Port, Is.EqualTo(standbyPostmasterMock.Port)); } - [Test] - public void DataSource_with_TargetSessionAttributes_is_not_supported() - { - var builder = new NpgsqlDataSourceBuilder("Host=foo,bar;Target Session Attributes=primary"); - - Assert.That(() => builder.BuildMultiHost(), Throws.Exception.TypeOf() - .With.Message.EqualTo(NpgsqlStrings.CannotSpecifyTargetSessionAttributes)); - } - [Test] public async Task BuildMultiHost_with_single_host_is_supported() { @@ -1171,7 +1244,20 @@ public async Task LoadBalancing_is_fair_if_first_host_is_down([Values]TargetSess static string MultipleHosts(params PgPostmasterMock[] postmasters) => string.Join(",", postmasters.Select(p => $"{p.Host}:{p.Port}")); - class DisposableWrapper(IEnumerable disposables) : IAsyncDisposable + static string? TargetSessionAttributesAsString(TargetSessionAttributes targetSessionAttributes) + => targetSessionAttributes switch + { + TargetSessionAttributes.Any => "Any", + TargetSessionAttributes.Primary => "Primary", + TargetSessionAttributes.Standby => "Standby", + TargetSessionAttributes.PreferPrimary => "Prefer-Primary", + TargetSessionAttributes.PreferStandby => "Prefer-Standby", + TargetSessionAttributes.ReadOnly => "Read-Only", + TargetSessionAttributes.ReadWrite => "Read-Write", + _ => null + }; + + sealed class DisposableWrapper(IEnumerable disposables) : IAsyncDisposable { public async ValueTask DisposeAsync() { From 5d073dad647994754ad672c134474c0fd4869662 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 5 Oct 2025 21:45:35 +0300 Subject: [PATCH 073/155] Add support for multiple client certificates (#6162) Fixes #6152 --- src/Npgsql/Internal/NpgsqlConnector.cs | 78 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 8018abe56d..236b5375aa 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -283,7 +283,7 @@ internal bool PostgresCancellationPerformed #pragma warning disable CA1859 // We're casting to IDisposable to not explicitly reference X509Certificate2 for NativeAOT // TODO: probably pointless now, needs to be rechecked - IDisposable? _certificate; + List? _certificates; #pragma warning restore CA1859 internal NpgsqlLoggingConfiguration LoggingConfiguration { get; } @@ -1066,36 +1066,61 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, { var password = Settings.SslPassword; - X509Certificate2? cert = null; if (!string.Equals(Path.GetExtension(certPath), ".pfx", StringComparison.OrdinalIgnoreCase)) { // It's PEM time var keyPath = Settings.SslKey ?? PostgresEnvironment.SslKey ?? PostgresEnvironment.SslKeyDefault; - cert = string.IsNullOrEmpty(password) + + // With PEM certificates we might have multiple certificates in a single file + // Where the first one is a leaf (and it has to have a private key) + // And others are intermediate between it and CA cert + // To support this, we first load the leaf certificate with private key + // And then we load everything else including the leaf, but without private key + // And afterwards we just get rid of the duplicate + var firstClientCert = string.IsNullOrEmpty(password) ? X509Certificate2.CreateFromPemFile(certPath, keyPath) : X509Certificate2.CreateFromEncryptedPemFile(certPath, password, keyPath); + clientCertificates.Add(firstClientCert); + + clientCertificates.ImportFromPemFile(certPath); + clientCertificates[1].Dispose(); + clientCertificates.RemoveAt(1); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Windows crypto API has a bug with pem certs - // See #3650 - using var previousCert = cert; + for (var i = 0; i < clientCertificates.Count; i++) + { + var cert = clientCertificates[i]; + + // Windows crypto API has a bug with pem certs + // See #3650 + using var previousCert = cert; #if NET9_0_OR_GREATER - cert = X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null); + cert = X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null); #else - cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12)); + cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12)); #endif + clientCertificates[i] = cert; + } } } + // If it's empty, it's probably PFX + if (clientCertificates.Count == 0) + { #if NET9_0_OR_GREATER - // If it's null, it's probably PFX - cert ??= X509CertificateLoader.LoadPkcs12FromFile(certPath, password); + var certs = X509CertificateLoader.LoadPkcs12CollectionFromFile(certPath, password); + clientCertificates.AddRange(certs); #else - cert ??= new X509Certificate2(certPath, password); + var cert = new X509Certificate2(certPath, password); + clientCertificates.Add(cert); #endif - clientCertificates.Add(cert); + } - _certificate = cert; + var certificates = new List(); + foreach (var certificate in clientCertificates) + certificates.Add(certificate); + _certificates = certificates; } try @@ -1127,6 +1152,20 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, certificateValidationCallback = SslVerifyFullValidation; } + SslStreamCertificateContext? clientCertificateContext = null; + if (clientCertificates.Count > 0) + { + // SslClientAuthenticationOptions.ClientCertificates only sends trusted certificates or if they have private key + // Which makes us unable to send intermediate certificates + // Work around this by specifying the first certificate as target + // And others as additional + // See https://github.com/dotnet/runtime/issues/26323 + var clientCertificate = clientCertificates[0]; + clientCertificates.RemoveAt(0); + + clientCertificateContext = SslStreamCertificateContext.Create(clientCertificate, clientCertificates); + } + var host = Host; timeout.CheckAndApply(this); @@ -1136,7 +1175,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, var sslStreamOptions = new SslClientAuthenticationOptions { TargetHost = host, - ClientCertificates = clientCertificates, + ClientCertificateContext = clientCertificateContext, EnabledSslProtocols = SslProtocols.None, CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, RemoteCertificateValidationCallback = certificateValidationCallback, @@ -1184,8 +1223,8 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, } catch { - _certificate?.Dispose(); - _certificate = null; + _certificates?.ForEach(x => x.Dispose()); + _certificates = null; throw; } @@ -2525,11 +2564,8 @@ void Cleanup() PostgresParameters.Clear(); _currentCommand = null; - if (_certificate is not null) - { - _certificate.Dispose(); - _certificate = null; - } + _certificates?.ForEach(x => x.Dispose()); + _certificates = null; } void GenerateResetMessage() From 5ede53c5ba8cfc221b37fb2d0dcf00ad830dac80 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 5 Oct 2025 21:47:19 +0300 Subject: [PATCH 074/155] Fix getting wrong schema with CommandBehavior.SchemaOnly and autoprepare (#6040) Fixes #6038 --- src/Npgsql/NpgsqlCommand.cs | 19 +++++++-- src/Npgsql/NpgsqlDataReader.cs | 16 +++----- test/Npgsql.Tests/AutoPrepareTests.cs | 57 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index d5ad24fdb2..8f6816b657 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1118,11 +1118,24 @@ async Task WriteExecuteSchemaOnly(NpgsqlConnector connector, bool async, bool fl await new TaskSchedulerAwaitable(ConstrainedConcurrencyScheduler); var batchCommand = InternalBatchCommands[i]; + var pStatement = batchCommand.PreparedStatement; + + pStatement?.RefreshLastUsed(); + + Debug.Assert(batchCommand.FinalCommandText is not null); + + if (pStatement != null && !batchCommand.IsPreparing) + { + // Prepared, we already have the RowDescription + Debug.Assert(pStatement.IsPrepared); + continue; + } - if (batchCommand.PreparedStatement?.State == PreparedState.Prepared) - continue; // Prepared, we already have the RowDescription + // We may have a prepared statement that replaces an existing statement - close the latter first. + if (pStatement?.StatementBeingReplaced != null) + await connector.WriteClose(StatementOrPortal.Statement, pStatement.StatementBeingReplaced.Name!, async, cancellationToken).ConfigureAwait(false); - await connector.WriteParse(batchCommand.FinalCommandText!, batchCommand.StatementName, + await connector.WriteParse(batchCommand.FinalCommandText, batchCommand.StatementName, batchCommand.CurrentParametersReadOnly, async, cancellationToken).ConfigureAwait(false); await connector.WriteDescribe(StatementOrPortal.Statement, batchCommand.StatementName, async, cancellationToken).ConfigureAwait(false); diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 86963afd4a..10c383f14d 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -713,7 +713,11 @@ async Task NextResultSchemaOnly(bool async, bool isConsuming = false, Canc break; case BackendMessageCode.RowDescription: // We have a resultset - RowDescription = _statements[StatementIndex].Description = (RowDescriptionMessage)msg; + // RowDescription messages are cached on the connector, but if we're auto-preparing, we need to + // clone our own copy which will last beyond the lifetime of this invocation. + RowDescription = _statements[StatementIndex].Description = preparedStatement == null + ? (RowDescriptionMessage)msg + : ((RowDescriptionMessage)msg).Clone(); Command.FixupRowDescription(RowDescription, StatementIndex == 0); break; default: @@ -734,17 +738,7 @@ async Task NextResultSchemaOnly(bool async, bool isConsuming = false, Canc // Found a resultset if (RowDescription is not null) - { - if (ColumnInfoCache?.Length >= ColumnCount) - Array.Clear(ColumnInfoCache, 0, ColumnCount); - else - { - if (ColumnInfoCache is { } cache) - ArrayPool.Shared.Return(cache, clearArray: true); - ColumnInfoCache = ArrayPool.Shared.Rent(ColumnCount); - } return true; - } } State = ReaderState.Consumed; diff --git a/test/Npgsql.Tests/AutoPrepareTests.cs b/test/Npgsql.Tests/AutoPrepareTests.cs index 00d9455147..b35fe7c5d3 100644 --- a/test/Npgsql.Tests/AutoPrepareTests.cs +++ b/test/Npgsql.Tests/AutoPrepareTests.cs @@ -538,6 +538,63 @@ public async Task SchemaOnly() await cmd.ExecuteScalarAsync(); } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6038")] + public async Task Auto_prepared_schema_only_correct_schema() + { + await using var dataSource = CreateDataSource(csb => + { + csb.MaxAutoPrepare = 1; + csb.AutoPrepareMinUsages = 5; + }); + await using var connection = await dataSource.OpenConnectionAsync(); + var table1 = await CreateTempTable(connection, "foo int"); + var table2 = await CreateTempTable(connection, "bar int"); + + await using var cmd = connection.CreateCommand(); + cmd.CommandText = $"SELECT * FROM {table1}"; + for (var i = 0; i < 5; i++) + { + // Make sure we prepare the first query + await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { } + } + + cmd.CommandText = $"SELECT * FROM {table2}"; + // The second query will load RowDescription, which is a singleton on NpgsqlConnector + // This shouldn't affect the first query, because we create a copy of RowDescription on prepare + await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { } + + cmd.CommandText = $"SELECT * FROM {table1}"; + // If we indeed made a copy of RowDescription on prepare, we should get the column for the first query and not for the second + await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo); + var columns = await reader.GetColumnSchemaAsync(); + Assert.That(columns.Count, Is.EqualTo(1)); + Assert.That(columns[0].ColumnName, Is.EqualTo("foo")); + } + + [Test] + public async Task Auto_prepared_schema_only_replace() + { + await using var dataSource = CreateDataSource(csb => + { + csb.MaxAutoPrepare = 1; + csb.AutoPrepareMinUsages = 5; + }); + await using var connection = await dataSource.OpenConnectionAsync(); + + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1"; + for (var i = 0; i < 5; i++) + { + await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { } + } + + cmd.CommandText = "SELECT 2"; + for (var i = 0; i < 5; i++) + { + await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { } + } + } + [Test] public async Task Auto_prepared_statement_invalidation() { From 5fd06df3a13fb67e001c62ec9211b08050ec3b01 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 5 Oct 2025 22:12:17 +0300 Subject: [PATCH 075/155] Remove timeout translation from NpgsqlReadBuffer (#6126) Fixes #6122 --- src/Npgsql/Internal/NpgsqlConnector.cs | 20 +++++++++++++++----- src/Npgsql/Internal/NpgsqlReadBuffer.cs | 17 ++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 236b5375aa..4ef0bd44dc 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -276,7 +276,7 @@ internal bool PostgresCancellationPerformed internal bool UserCancellationRequested => _userCancellationRequested; internal CancellationToken UserCancellationToken { get; set; } internal bool AttemptPostgresCancellation { get; private set; } - static readonly TimeSpan _cancelImmediatelyTimeout = TimeSpan.FromMilliseconds(-1); + static readonly TimeSpan _cancelImmediatelyTimeout = TimeSpan.Zero; static readonly SslApplicationProtocol _alpnProtocol = new("postgresql"); @@ -2082,6 +2082,8 @@ void PerformUserCancellationUnsynchronized() var cancellationTimeout = Settings.CancellationTimeout; if (PerformPostgresCancellation() && cancellationTimeout >= 0) { + // TODO: according to docs, we treat 0 timeout as infinite, yet we do not change the actual value + // We should revisit this here and in NpgsqlReadBuffer if (cancellationTimeout > 0) { ReadBuffer.Timeout = TimeSpan.FromMilliseconds(cancellationTimeout); @@ -2799,8 +2801,9 @@ UserAction DoStartUserAction(ConnectorState newState, NpgsqlCommand? command, // We reset the ReadBuffer.Timeout for every user action, so it wouldn't leak from the previous query or action // For example, we might have successfully cancelled the previous query (so the connection is not broken) - // But the next time, we call the Prepare, which doesn't set it's own timeout - ReadBuffer.Timeout = TimeSpan.FromSeconds(command?.CommandTimeout ?? Settings.CommandTimeout); + // But the next time, we call the Prepare, which doesn't set its own timeout + var timeoutSeconds = command?.CommandTimeout ?? Settings.CommandTimeout; + ReadBuffer.Timeout = timeoutSeconds > 0 ? TimeSpan.FromSeconds(timeoutSeconds) : Timeout.InfiniteTimeSpan; return new UserAction(this); } @@ -2935,12 +2938,15 @@ internal async Task Wait(bool async, int timeout, CancellationToken cancel await Flush(async, cancellationToken).ConfigureAwait(false); var keepaliveMs = Settings.KeepAlive * 1000; + var isTimeoutInfinite = timeout <= 0; while (true) { cancellationToken.ThrowIfCancellationRequested(); - var timeoutForKeepalive = _isKeepAliveEnabled && (timeout <= 0 || keepaliveMs < timeout); - ReadBuffer.Timeout = TimeSpan.FromMilliseconds(timeoutForKeepalive ? keepaliveMs : timeout); + var timeoutForKeepalive = _isKeepAliveEnabled && (isTimeoutInfinite || keepaliveMs < timeout); + ReadBuffer.Timeout = timeoutForKeepalive + ? TimeSpan.FromMilliseconds(keepaliveMs) + : isTimeoutInfinite ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(timeout); try { var msg = await ReadMessageWithNotifications(async).ConfigureAwait(false); @@ -3000,7 +3006,11 @@ internal async Task Wait(bool async, int timeout, CancellationToken cancel } if (timeout > 0) + { timeout -= (keepaliveMs + (int)Stopwatch.GetElapsedTime(keepaliveStartTimestamp).TotalMilliseconds); + // Make sure we don't accidentally set -1 as a timeout (because it's infinite) + timeout = Math.Max(timeout, 0); + } } } diff --git a/src/Npgsql/Internal/NpgsqlReadBuffer.cs b/src/Npgsql/Internal/NpgsqlReadBuffer.cs index d8622fc7a1..0f91bad9d4 100644 --- a/src/Npgsql/Internal/NpgsqlReadBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlReadBuffer.cs @@ -36,25 +36,16 @@ sealed partial class NpgsqlReadBuffer : IDisposable internal ResettableCancellationTokenSource Cts { get; } readonly MetricsReporter? _metricsReporter; - TimeSpan _preTranslatedTimeout = TimeSpan.Zero; - /// /// Timeout for sync and async reads /// internal TimeSpan Timeout { - get => _preTranslatedTimeout; + get => Cts.Timeout; set { - if (_preTranslatedTimeout != value) + if (Cts.Timeout != value) { - _preTranslatedTimeout = value; - - if (value == TimeSpan.Zero) - value = InfiniteTimeSpan; - else if (value < TimeSpan.Zero) - value = TimeSpan.Zero; - Debug.Assert(_underlyingSocket != null); _underlyingSocket.ReceiveTimeout = (int)value.TotalMilliseconds; @@ -189,7 +180,7 @@ int ReadWithTimeout(Span buffer) async ValueTask ReadWithTimeoutAsync(Memory buffer, CancellationToken cancellationToken) { - var finalCt = Timeout != TimeSpan.Zero + var finalCt = Timeout != InfiniteTimeSpan ? Cts.Start(cancellationToken) : Cts.Reset(); @@ -289,7 +280,7 @@ static async ValueTask EnsureLong( buffer.ReadPosition = 0; } - var finalCt = async && buffer.Timeout != TimeSpan.Zero + var finalCt = async && buffer.Timeout != InfiniteTimeSpan ? buffer.Cts.Start() : buffer.Cts.Reset(); From f5ac3a85e81f67721570c12712d70797d5fbf6ba Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Sun, 5 Oct 2025 22:31:10 +0300 Subject: [PATCH 076/155] Add a connection string parameter to control NpgsqlException.BatchCommand (#6098) Closes #6042 --- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 18 ++++++++++++++++++ src/Npgsql/NpgsqlDataReader.cs | 7 +++++-- src/Npgsql/NpgsqlException.cs | 1 + src/Npgsql/PublicAPI.Unshipped.txt | 2 ++ test/Npgsql.Tests/BatchTests.cs | 9 +++++++-- test/Npgsql.Tests/ReaderTests.cs | 20 ++++++++++++++------ 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index 7200e45130..5b2d13a1d0 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -683,6 +683,24 @@ public bool IncludeErrorDetail } bool _includeErrorDetail; + /// + /// When enabled, failed statements are included on . + /// + [Category("Security")] + [Description("When enabled, failed batched commands are included on NpgsqlException.BatchCommand.")] + [DisplayName("Include Failed Batched Command")] + [NpgsqlConnectionStringProperty] + public bool IncludeFailedBatchedCommand + { + get => _includeFailedBatchedCommand; + set + { + _includeFailedBatchedCommand = value; + SetValue(nameof(IncludeFailedBatchedCommand), value); + } + } + bool _includeFailedBatchedCommand; + /// /// Controls whether channel binding is required, disabled or preferred, depending on server support. /// diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 10c383f14d..32d66090a9 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -516,7 +516,8 @@ async Task NextResult(bool async, bool isConsuming = false, CancellationTo var statement = _statements[StatementIndex]; // Reference the triggering statement from the exception - postgresException.BatchCommand = statement; + if (Connector.Settings.IncludeFailedBatchedCommand) + postgresException.BatchCommand = statement; // Prevent the command or batch from being recycled (by the connection) when it's disposed. This is important since // the exception is very likely to escape the using statement of the command, and by that time some other user may @@ -754,7 +755,9 @@ async Task NextResultSchemaOnly(bool async, bool isConsuming = false, Canc // Reference the triggering statement from the exception if (e is PostgresException postgresException && StatementIndex >= 0 && StatementIndex < _statements.Count) { - postgresException.BatchCommand = _statements[StatementIndex]; + // Reference the triggering statement from the exception + if (Connector.Settings.IncludeFailedBatchedCommand) + postgresException.BatchCommand = _statements[StatementIndex]; // Prevent the command or batch from being recycled (by the connection) when it's disposed. This is important since // the exception is very likely to escape the using statement of the command, and by that time some other user may diff --git a/src/Npgsql/NpgsqlException.cs b/src/Npgsql/NpgsqlException.cs index 89543b0b50..9e2dfe9ee0 100644 --- a/src/Npgsql/NpgsqlException.cs +++ b/src/Npgsql/NpgsqlException.cs @@ -46,6 +46,7 @@ public override bool IsTransient => InnerException is IOException or SocketException or TimeoutException or NpgsqlException { IsTransient: true }; /// + /// This property is null unless in connection string is set to true. public new NpgsqlBatchCommand? BatchCommand { get; set; } /// diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index a1d261ead0..43905953db 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Act Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.get -> Npgsql.GssEncryptionMode Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.set -> void +Npgsql.NpgsqlConnectionStringBuilder.IncludeFailedBatchedCommand.get -> bool +Npgsql.NpgsqlConnectionStringBuilder.IncludeFailedBatchedCommand.set -> void Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string? Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation diff --git a/test/Npgsql.Tests/BatchTests.cs b/test/Npgsql.Tests/BatchTests.cs index 960e6028f9..837ace48fd 100644 --- a/test/Npgsql.Tests/BatchTests.cs +++ b/test/Npgsql.Tests/BatchTests.cs @@ -12,7 +12,7 @@ namespace Npgsql.Tests; [TestFixture(MultiplexingMode.Multiplexing, CommandBehavior.Default)] [TestFixture(MultiplexingMode.NonMultiplexing, CommandBehavior.SequentialAccess)] [TestFixture(MultiplexingMode.Multiplexing, CommandBehavior.SequentialAccess)] -public class BatchTests : MultiplexingTestBase +public class BatchTests : MultiplexingTestBase, IDisposable { #region Parameters @@ -477,7 +477,7 @@ public async Task Batch_with_multiple_errors([Values] bool withErrorBarriers) public async Task Batch_close_dispose_reader_with_multiple_errors([Values] bool withErrorBarriers, [Values] bool dispose) { // Create a temp pool since we dispose the reader (and check the state afterwards) and it can be reused by another connection - await using var dataSource = CreateDataSource(); + await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = true); await using var conn = await dataSource.OpenConnectionAsync(); var table = await CreateTempTable(conn, "id INT"); @@ -804,11 +804,16 @@ public async Task Batch_dispose_reuse() readonly CommandBehavior Behavior; // ReSharper restore InconsistentNaming + NpgsqlDataSource? _dataSource; + protected override NpgsqlDataSource DataSource => _dataSource ??= CreateDataSource(csb => csb.IncludeFailedBatchedCommand = true); + public BatchTests(MultiplexingMode multiplexingMode, CommandBehavior behavior) : base(multiplexingMode) { Behavior = behavior; IsSequential = (Behavior & CommandBehavior.SequentialAccess) != 0; } + public void Dispose() => DataSource.Dispose(); + #endregion } diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index 432a9aa327..2d1e7040cd 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -623,9 +623,10 @@ await conn.ExecuteNonQueryAsync($@" } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")] - public async Task NpgsqlException_references_BatchCommand_with_single_command() + public async Task NpgsqlException_references_BatchCommand_with_single_command([Values] bool includeFailedBatchedCommand) { - await using var conn = await OpenConnectionAsync(); + await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = includeFailedBatchedCommand); + await using var conn = await dataSource.OpenConnectionAsync(); var function = await GetTempFunctionName(conn); await conn.ExecuteNonQueryAsync($@" @@ -638,7 +639,10 @@ await conn.ExecuteNonQueryAsync($@" cmd.CommandText = $"SELECT {function}()"; var exception = Assert.ThrowsAsync(() => cmd.ExecuteReaderAsync(Behavior))!; - Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[0])); + if (includeFailedBatchedCommand) + Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[0])); + else + Assert.That(exception.BatchCommand, Is.Null); // Make sure the command isn't recycled by the connection when it's disposed - this is important since internal command // resources are referenced by the exception above, which is very likely to escape the using statement of the command. @@ -648,9 +652,10 @@ await conn.ExecuteNonQueryAsync($@" } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")] - public async Task NpgsqlException_references_BatchCommand_with_multiple_commands() + public async Task NpgsqlException_references_BatchCommand_with_multiple_commands([Values] bool includeFailedBatchedCommand) { - await using var conn = await OpenConnectionAsync(); + await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = includeFailedBatchedCommand); + await using var conn = await dataSource.OpenConnectionAsync(); var function = await GetTempFunctionName(conn); await conn.ExecuteNonQueryAsync($@" @@ -665,7 +670,10 @@ await conn.ExecuteNonQueryAsync($@" await using (var reader = await cmd.ExecuteReaderAsync(Behavior)) { var exception = Assert.ThrowsAsync(() => reader.NextResultAsync())!; - Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[1])); + if (includeFailedBatchedCommand) + Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[1])); + else + Assert.That(exception.BatchCommand, Is.Null); } // Make sure the command isn't recycled by the connection when it's disposed - this is important since internal command From 0fc7f66c67271a01ab41cefe35af8b2a92c414e7 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 5 Oct 2025 21:53:15 +0200 Subject: [PATCH 077/155] Add implicit cast from .NET IPNetwork to NpgsqlInet (#6232) Helps EFCore.PG especially, continues #5821. --- .../Converters/Networking/IPNetworkConverter.cs | 13 +++++++++++-- src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs | 7 +++++++ src/Npgsql/PublicAPI.Unshipped.txt | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs b/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs index 77714edf29..6fc7b5401e 100644 --- a/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs +++ b/src/Npgsql/Internal/Converters/Networking/IPNetworkConverter.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; // ReSharper disable once CheckNamespace namespace Npgsql.Internal.Converters; @@ -18,5 +19,13 @@ protected override IPNetwork ReadCore(PgReader reader) } protected override void WriteCore(PgWriter writer, IPNetwork value) - => NpgsqlInetConverter.WriteImpl(writer, (value.BaseAddress, (byte)value.PrefixLength), isCidr: true); + => NpgsqlInetConverter.WriteImpl( + writer, + ( + value.BaseAddress, + value.PrefixLength <= byte.MaxValue + ? (byte)value.PrefixLength + : throw new ArgumentOutOfRangeException(nameof(value), "IPNetwork.PrefixLength is too large to fit in a byte") + ), + isCidr: true); } diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs index 7d5eedf7af..4736ca00ec 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs @@ -469,6 +469,13 @@ public static explicit operator IPAddress(NpgsqlInet inet) public static implicit operator NpgsqlInet(IPAddress ip) => new(ip); + public static implicit operator NpgsqlInet(IPNetwork cidr) + => new( + cidr.BaseAddress, + cidr.PrefixLength <= byte.MaxValue + ? (byte)cidr.PrefixLength + : throw new ArgumentOutOfRangeException(nameof(cidr), "IPNetwork.PrefixLength is too large to fit in a byte")); + public void Deconstruct(out IPAddress address, out byte netmask) { address = Address; diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 43905953db..8de71da9e5 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -86,4 +86,5 @@ Npgsql.NpgsqlConnection.ReloadTypesAsync(System.Threading.CancellationToken canc *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! +static NpgsqlTypes.NpgsqlInet.implicit operator NpgsqlTypes.NpgsqlInet(System.Net.IPNetwork cidr) -> NpgsqlTypes.NpgsqlInet static readonly NpgsqlTypes.NpgsqlTsVector.Empty -> NpgsqlTypes.NpgsqlTsVector! From 92a455a3a1cc7bd71a009f7514ecb28fa54f2d23 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 5 Oct 2025 22:03:24 +0200 Subject: [PATCH 078/155] Set version to 10.0.0-rc.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index de99b4fd8c..a3a4b11e2a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.0 + 10.0.0-rc.1 latest true enable From 0af34c82c5f1c4192902ec73c5f1cdb026475276 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 5 Oct 2025 22:06:19 +0200 Subject: [PATCH 079/155] Bump version to 10.0.0-rc.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a3a4b11e2a..56ff636631 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.0-rc.1 + 10.0.0-rc.2 latest true enable From 7042543eabaf185f281c7b79cf9907c9c804f58e Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 5 Oct 2025 22:18:27 +0200 Subject: [PATCH 080/155] Use dotnet SDK 10.0.100-rc.1 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 9f1e930171..838198f254 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.200", + "version": "10.0.100-rc.1.25451.107", "rollForward": "latestMajor", "allowPrerelease": false } From 91cc5c18273594366833aad9d22734aee4305867 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:18:48 +0000 Subject: [PATCH 081/155] Bump NUnit3TestAdapter from 5.1.0 to 5.2.0 (#6236) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f6c36102a..2f2363af80 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + From ac774b32c0d72c9227fc460401a849523e1cc14b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 23:29:38 +0200 Subject: [PATCH 082/155] Bump Microsoft.Data.SqlClient from 6.1.1 to 6.1.2 (#6242) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f2363af80..7eec3856fc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,7 +34,7 @@ - + From 4cc0884cca95055b5d8a7a7e8d9253628098affb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 23:48:12 +0200 Subject: [PATCH 083/155] Bump github/codeql-action from 3 to 4 (#6239) --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5a465ad42e..666e98fc85 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,7 +56,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -87,4 +87,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 From 154604112d6fb12175082f2d79a75800737a2442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:10:08 +0200 Subject: [PATCH 084/155] Bump OpenTelemetry.Api from 1.13.0 to 1.13.1 (#6245) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7eec3856fc..17a7ffc37c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + From 6d2f348d9f9121815769e2ba4c1091087bae74a2 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 15 Oct 2025 16:30:19 +0300 Subject: [PATCH 085/155] Fix a few issues with type mappings for schema generator (#6241) Fixes #6240 --- 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 b47422e830..f9643adfd5 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 2391cf54f0..f892670d96 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 2d1e7040cd..aa39a38467 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 92be7c724a642e2c695134bfe006a18ab11a4ce4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 17 Oct 2025 10:21:59 +0200 Subject: [PATCH 086/155] Bump dependencies to 10.0.0-rc.2 (#6260) --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 17a7ffc37c..1434e03287 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - - + + @@ -21,8 +21,8 @@ - - + + @@ -30,7 +30,7 @@ - + From 960050f5567c6ad05a34ab6447e47ee76c8f177f Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Fri, 24 Oct 2025 14:26:07 +0300 Subject: [PATCH 087/155] Fix infinite loop when a connector is closed while concurrently consuming result set (#6265) Fixes #6264 --- src/Npgsql/Internal/NpgsqlConnector.cs | 2 +- src/Npgsql/NpgsqlDataReader.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 4ef0bd44dc..e4c37f061d 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -467,7 +467,7 @@ internal ConnectorState State /// /// Returns whether the connector is open, regardless of any task it is currently performing /// - bool IsConnected => State is not (ConnectorState.Closed or ConnectorState.Connecting or ConnectorState.Broken); + internal bool IsConnected => State is not (ConnectorState.Closed or ConnectorState.Connecting or ConnectorState.Broken); internal bool IsReady => State == ConnectorState.Ready; internal bool IsClosed => State == ConnectorState.Closed; diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 32d66090a9..585536da18 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -984,11 +984,14 @@ async Task Consume(bool async, Exception? firstException = null) // state for auto-prepared statements // // The only exception is when the connector is broken (which can happen in the middle of consuming) - // As then there is no point in going forward + // As then there is no point in going forward. + // An exception to the exception above is when connector is concurrently closed while + // the reader is still going over the result set. + // While this is undefined behavior and user error, we should try to at least do our best to not loop indefinitely. // // While we can also check our local state (State == Closed) // It's probably better to rely on connector since it's private and its state can't be changed - while (!Connector.IsBroken) + while (Connector.IsConnected) { try { From c746805c9d43b5941ca16ec99ee9939561d6074e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:10:27 +0200 Subject: [PATCH 088/155] Bump actions/upload-artifact from 4 to 5 (#6266) --- .github/workflows/build.yml | 4 ++-- .github/workflows/native-aot.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f37d7e432f..ff7a6581f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -360,7 +360,7 @@ jobs: run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true - name: Upload artifacts (nupkg) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Npgsql.CI path: nupkgs @@ -392,7 +392,7 @@ jobs: run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Npgsql.Release path: nupkgs diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index ecc57d51a8..ef6d7b96b5 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -170,21 +170,21 @@ jobs: run: dotnet run --project test/MStatDumper/MStatDumper.csproj -c release -f ${{ matrix.tfm }} -- "test/Npgsql.NativeAotTests/obj/Release/net9.0/linux-x64/native/Npgsql.NativeAotTests.mstat" md >> $GITHUB_STEP_SUMMARY - name: Upload mstat - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: npgsql.mstat path: "test/Npgsql.NativeAotTests/obj/Release/${{ matrix.tfm }}/linux-x64/native/Npgsql.NativeAotTests.mstat" retention-days: 3 - name: Upload codedgen dgml - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: npgsql.codegen.dgml.xml path: "test/Npgsql.NativeAotTests/obj/Release/${{ matrix.tfm }}/linux-x64/native/Npgsql.NativeAotTests.codegen.dgml.xml" retention-days: 3 - name: Upload scan dgml - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: npgsql.scan.dgml.xml path: "test/Npgsql.NativeAotTests/obj/Release/${{ matrix.tfm }}/linux-x64/native/Npgsql.NativeAotTests.scan.dgml.xml" From 1d1ea6f7e4e8cccb8d65b60ed20592a08611c9b3 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 27 Oct 2025 14:01:44 +0100 Subject: [PATCH 089/155] Fix streaming threshold value (#6269) Closes #5978 --- src/Npgsql/Internal/Converters/JsonConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/Converters/JsonConverter.cs b/src/Npgsql/Internal/Converters/JsonConverter.cs index 77157875b3..074575e4e1 100644 --- a/src/Npgsql/Internal/Converters/JsonConverter.cs +++ b/src/Npgsql/Internal/Converters/JsonConverter.cs @@ -107,8 +107,8 @@ public override ValueTask WriteAsync(PgWriter writer, T? value, CancellationToke static class JsonConverter { public const byte JsonbProtocolVersion = 1; - // We pick a value that is the largest multiple of 4096 that is still smaller than the large object heap threshold (85K). - const int StreamingThreshold = 81920; + // Largest value that is a power of 2 and a multiple of 4096 while staying under the large object heap threshold (85K). + const int StreamingThreshold = 65536; public static bool TryReadStream(bool jsonb, Encoding encoding, PgReader reader, out int byteCount, [NotNullWhen(true)]out Stream? stream) { From 62fd0ad621dab74efdfde07a0b698b7d5a9f86d0 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 27 Oct 2025 21:59:28 +0100 Subject: [PATCH 090/155] Respect configured schemas in enum field loading (#6268) Closes #6246 --- 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 9f6a662eaeaf92560ac516aa8947b5b58109bb91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:43:09 +0100 Subject: [PATCH 091/155] Bump Scriban.Signed from 6.4.0 to 6.5.0 (#6273) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1434e03287..2643731072 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,7 +16,7 @@ - + From 153b91fc656ffef2f62bece778f6d9a88dbbbac3 Mon Sep 17 00:00:00 2001 From: Artem Serostanov Date: Wed, 29 Oct 2025 17:53:42 +0300 Subject: [PATCH 092/155] Incorrect multi-threading synchronization in NpgsqlDataSource.UpdateDatabaseState() (#6114) Co-authored-by: Serostanov Artem Sergeevich --- 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 cc177602db..080906ca26 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -424,7 +424,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 dd3b003a0d90cc948d2cba3ed9f088891a01e188 Mon Sep 17 00:00:00 2001 From: Bruce Bowyer-Smyth Date: Thu, 30 Oct 2025 01:37:22 +1000 Subject: [PATCH 093/155] Additional GetBytes/GetStream tests (#5934) --- test/Npgsql.Tests/ReaderTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index aa39a38467..0d27bc5d03 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -817,6 +817,7 @@ public async Task Null() Assert.That(reader.GetFieldValue(i), Is.EqualTo(DBNull.Value)); Assert.That(reader.GetProviderSpecificValue(i), Is.EqualTo(DBNull.Value)); Assert.That(() => reader.GetString(i), Throws.Exception.TypeOf()); + Assert.That(() => reader.GetStream(i), Throws.Exception.TypeOf()); } } @@ -1436,6 +1437,25 @@ public async Task GetStream_second_time_throws([Values(true, false)] bool isAsyn Throws.Exception.TypeOf()); } + [Test] + public async Task GetBytes_before_getstream([Values(true, false)] bool isAsync) + { + var expected = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var streamGetter = BuildStreamGetter(isAsync); + + using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand($"SELECT {EncodeByteaHex(expected)}::bytea", conn); + using var reader = await cmd.ExecuteReaderAsync(Behavior); + + await reader.ReadAsync(); + + // GetBytes with null buffer won't consume column in any way + Assert.That(reader.GetBytes(0, 0, null, 0, 0), Is.EqualTo(expected.Length), "Bad column length"); + + using var stream = await streamGetter(reader, 0); + Assert.That(stream.Length, Is.EqualTo(expected.Length)); + } + public static IEnumerable GetStreamCases() { var binary = MemoryMarshal From 2ef750c1a4ed5dd5d7e19f9cb7c840ebb23a5fb8 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Wed, 29 Oct 2025 17:23:19 +0100 Subject: [PATCH 094/155] Fix inclusive infinity upper bound interval conversion (#6270) Closes #6080 --- .../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 19d62f642d6d062d08111aa5c79643bc5a6be4d2 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Windisch Date: Wed, 29 Oct 2025 13:44:52 -0300 Subject: [PATCH 095/155] Wrap GetHostAddresses/Async calls to catch SocketException (#5664) Closes #5606 --- src/Npgsql/Internal/NpgsqlConnector.cs | 52 +++++++++++++++++++------- test/Npgsql.Tests/ConnectionTests.cs | 33 ++++++++++++++++ test/Npgsql.Tests/TracingTests.cs | 2 +- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index e4c37f061d..2ceed5cfb9 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1232,10 +1232,24 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, void Connect(NpgsqlTimeout timeout) { - // Note that there aren't any timeout-able or cancellable DNS methods - var endpoints = NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath) - ? new EndPoint[] { new UnixDomainSocketEndPoint(socketPath) } - : IPAddressesToEndpoints(Dns.GetHostAddresses(Host), Port); + EndPoint[]? endpoints; + if (NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath)) + { + endpoints = [new UnixDomainSocketEndPoint(socketPath!)]; + } + else + { + // Note that there aren't any timeout-able or cancellable DNS methods + try + { + endpoints = IPAddressesToEndpoints(Dns.GetHostAddresses(Host), Port); + } + catch (SocketException ex) + { + throw new NpgsqlException(ex.Message, ex); + } + } + timeout.Check(); // Give each endpoint an equal share of the remaining time @@ -1302,16 +1316,28 @@ void Connect(NpgsqlTimeout timeout) async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken) { - Task GetHostAddressesAsync(CancellationToken ct) => - Dns.GetHostAddressesAsync(Host, ct); - // Whether the framework and/or the OS platform support Dns.GetHostAddressesAsync cancellation API or they do not, // we always fake-cancel the operation with the help of TaskTimeoutAndCancellation.ExecuteAsync. It stops waiting // and raises the exception, while the actual task may be left running. - var endpoints = NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath) - ? new EndPoint[] { new UnixDomainSocketEndPoint(socketPath) } - : IPAddressesToEndpoints(await TaskTimeoutAndCancellation.ExecuteAsync(GetHostAddressesAsync, timeout, cancellationToken).ConfigureAwait(false), - Port); + EndPoint[] endpoints; + if (NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath)) + { + endpoints = [new UnixDomainSocketEndPoint(socketPath)]; + } + else + { + IPAddress[] ipAddresses; + try + { + ipAddresses = await Dns.GetHostAddressesAsync(Host, cancellationToken) + .WaitAsync(timeout.CheckAndGetTimeLeft(), cancellationToken).ConfigureAwait(false); + } + catch (SocketException ex) + { + throw new NpgsqlException(ex.Message, ex); + } + endpoints = IPAddressesToEndpoints(ipAddresses, Port); + } // Give each endpoint an equal share of the remaining time var perEndpointTimeout = default(TimeSpan); @@ -1376,9 +1402,9 @@ Task ConnectAsync(CancellationToken ct) => } } - IPEndPoint[] IPAddressesToEndpoints(IPAddress[] ipAddresses, int port) + EndPoint[] IPAddressesToEndpoints(IPAddress[] ipAddresses, int port) { - var result = new IPEndPoint[ipAddresses.Length]; + var result = new EndPoint[ipAddresses.Length]; for (var i = 0; i < ipAddresses.Length; i++) result[i] = new IPEndPoint(ipAddresses[i], port); return result; diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index cda220a110..7ef94350fd 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Net.Security; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -313,6 +314,38 @@ public void Connect_timeout_cancel() Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); } + [Test] + public void Bad_hostname() + { + using var dataSource = CreateDataSource(csb => csb.Host = "hostname.that.does.not.exist"); + using var conn = dataSource.CreateConnection(); + + Assert.That( + () => conn.Open(), + Throws.Exception + .TypeOf() + .With + .Property(nameof(NpgsqlException.InnerException)) + .TypeOf() + ); + } + + [Test] + public void Bad_hostname_async() + { + using var dataSource = CreateDataSource(csb => csb.Host = "hostname.that.does.not.exist"); + using var conn = dataSource.CreateConnection(); + + Assert.That( + async () => await conn.OpenAsync(), + Throws.Exception + .TypeOf() + .With + .Property(nameof(NpgsqlException.InnerException)) + .TypeOf() + ); + } + #endregion #region Client Encoding diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 74e43f63a8..faec49c238 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -158,7 +158,7 @@ public async Task Error_open([Values] bool async) ActivitySource.AddActivityListener(activityListener); await using var dataSource = CreateDataSource(x => x.Host = "not-existing-host"); - var ex = Assert.ThrowsAsync(async () => + var ex = Assert.ThrowsAsync(async () => { await using var conn = async ? await dataSource.OpenConnectionAsync() From a06475b9378a23bc830c381380f22f9bb72198ed Mon Sep 17 00:00:00 2001 From: Dipankar Das Date: Wed, 29 Oct 2025 22:49:43 +0530 Subject: [PATCH 096/155] Add Deconstruct() for Npgsql types (#5695) Closes #5672 --- src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs | 49 +++++++++++++++++++++++++++ src/Npgsql/PublicAPI.Unshipped.txt | 9 +++++ 2 files changed, 58 insertions(+) diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs index 4736ca00ec..4f63a9defb 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlTypes.cs @@ -38,6 +38,8 @@ public override int GetHashCode() public override string ToString() => string.Format(CultureInfo.InvariantCulture, "({0},{1})", X, Y); + + public void Deconstruct(out double x, out double y) => (x, y) = (X, Y); } /// @@ -66,6 +68,8 @@ public override bool Equals(object? obj) public static bool operator ==(NpgsqlLine x, NpgsqlLine y) => x.Equals(y); public static bool operator !=(NpgsqlLine x, NpgsqlLine y) => !(x == y); + + public void Deconstruct(out double a, out double b, out double c) => (a, b, c) = (A, B, C); } /// @@ -103,6 +107,8 @@ public override bool Equals(object? obj) public static bool operator ==(NpgsqlLSeg x, NpgsqlLSeg y) => x.Equals(y); public static bool operator !=(NpgsqlLSeg x, NpgsqlLSeg y) => !(x == y); + + public void Deconstruct(out NpgsqlPoint start, out NpgsqlPoint end) => (start, end) = (Start, End); } /// @@ -178,6 +184,30 @@ void NormalizeBox() if (_upperRight.Y < _lowerLeft.Y) (_upperRight.Y, _lowerLeft.Y) = (_lowerLeft.Y, _upperRight.Y); } + + public void Deconstruct(out NpgsqlPoint lowerLeft, out NpgsqlPoint upperRight) + { + lowerLeft = LowerLeft; + upperRight = UpperRight; + } + + public void Deconstruct(out double left, out double right, out double bottom, out double top) + { + left = Left; + right = Right; + bottom = Bottom; + top = Top; + } + + public void Deconstruct(out double left, out double right, out double bottom, out double top, out double width, out double height) + { + left = Left; + right = Right; + bottom = Bottom; + top = Top; + width = Width; + height = Height; + } } /// @@ -413,6 +443,19 @@ public override string ToString() public override int GetHashCode() => HashCode.Combine(X, Y, Radius); + + public void Deconstruct(out double x, out double y, out double radius) + { + x = X; + y = Y; + radius = Radius; + } + + public void Deconstruct(out NpgsqlPoint center, out double radius) + { + center = Center; + radius = Radius; + } } /// @@ -562,6 +605,12 @@ public override bool Equals(object? o) public static bool operator ==(NpgsqlTid left, NpgsqlTid right) => left.Equals(right); public static bool operator !=(NpgsqlTid left, NpgsqlTid right) => !(left == right); public override string ToString() => $"({BlockNumber},{OffsetNumber})"; + + public void Deconstruct(out uint blockNumber, out ushort offsetNumber) + { + blockNumber = BlockNumber; + offsetNumber = OffsetNumber; + } } #pragma warning restore 1591 diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 8de71da9e5..e7c8061376 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -88,3 +88,12 @@ Npgsql.NpgsqlConnection.ReloadTypesAsync(System.Threading.CancellationToken canc *REMOVED*Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.TypeMapping.INpgsqlTypeMapper! static NpgsqlTypes.NpgsqlInet.implicit operator NpgsqlTypes.NpgsqlInet(System.Net.IPNetwork cidr) -> NpgsqlTypes.NpgsqlInet static readonly NpgsqlTypes.NpgsqlTsVector.Empty -> NpgsqlTypes.NpgsqlTsVector! +NpgsqlTypes.NpgsqlBox.Deconstruct(out double left, out double right, out double bottom, out double top) -> void +NpgsqlTypes.NpgsqlBox.Deconstruct(out double left, out double right, out double bottom, out double top, out double width, out double height) -> void +NpgsqlTypes.NpgsqlBox.Deconstruct(out NpgsqlTypes.NpgsqlPoint lowerLeft, out NpgsqlTypes.NpgsqlPoint upperRight) -> void +NpgsqlTypes.NpgsqlCircle.Deconstruct(out double x, out double y, out double radius) -> void +NpgsqlTypes.NpgsqlCircle.Deconstruct(out NpgsqlTypes.NpgsqlPoint center, out double radius) -> void +NpgsqlTypes.NpgsqlLine.Deconstruct(out double a, out double b, out double c) -> void +NpgsqlTypes.NpgsqlLSeg.Deconstruct(out NpgsqlTypes.NpgsqlPoint start, out NpgsqlTypes.NpgsqlPoint end) -> void +NpgsqlTypes.NpgsqlPoint.Deconstruct(out double x, out double y) -> void +NpgsqlTypes.NpgsqlTid.Deconstruct(out uint blockNumber, out ushort offsetNumber) -> void From 05e042760920afe1f09bab6a89a638b658a85c2e Mon Sep 17 00:00:00 2001 From: Sergiusz <38229504+KeterSCP@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:22:34 +0100 Subject: [PATCH 097/155] Explicitly set histogram bucket bounds (#6167) (#6168) Closes #6167 --- Directory.Packages.props | 3 +++ src/Npgsql/MetricsReporter.cs | 16 +++++++++++----- src/Npgsql/Npgsql.csproj | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2643731072..836b907a56 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,9 @@ + + + diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index 707193e553..a25a3173cb 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -1,7 +1,6 @@ -using System; - namespace Npgsql; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; @@ -9,7 +8,7 @@ namespace Npgsql; using System.Threading; // .NET docs on metric instrumentation: https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-instrumentation -// OpenTelemetry semantic conventions for database metric: https://opentelemetry.io/docs/specs/otel/metrics/semantic_conventions/database-metrics +// OpenTelemetry semantic conventions for database metric: https://opentelemetry.io/docs/specs/semconv/database/database-metrics sealed class MetricsReporter : IDisposable { const string Version = "0.1.0"; @@ -33,6 +32,11 @@ sealed class MetricsReporter : IDisposable static readonly List Reporters = []; + static readonly InstrumentAdvice ShortHistogramAdvice = new() + { + HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] + }; + CommandCounters _commandCounters; [StructLayout(LayoutKind.Explicit)] @@ -60,7 +64,8 @@ static MetricsReporter() CommandDuration = Meter.CreateHistogram( "db.client.commands.duration", unit: "s", - description: "The duration of database commands, in seconds."); + description: "The duration of database commands, in seconds.", + advice: ShortHistogramAdvice); BytesWritten = Meter.CreateCounter( "db.client.commands.bytes_written", @@ -85,7 +90,8 @@ static MetricsReporter() ConnectionCreateTime = Meter.CreateHistogram( "db.client.connections.create_time", unit: "s", - description: "The time it took to create a new connection."); + description: "The time it took to create a new connection.", + advice: ShortHistogramAdvice); // Observable metrics; these are for values we already track internally (and efficiently) inside the connection pool implementation. Meter.CreateObservableUpDownCounter( diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 096b6c762e..80c42ba561 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -14,6 +14,7 @@ + From 4f7cf71ff1b8d8057eac2e438d6d5401fabeeb22 Mon Sep 17 00:00:00 2001 From: Bruce Bowyer-Smyth Date: Thu, 30 Oct 2025 06:06:04 +1000 Subject: [PATCH 098/155] Reduce temporary string creation during types load (#5986) --- src/Npgsql/Internal/NpgsqlDatabaseInfo.cs | 6 +- src/Npgsql/Internal/Postgres/DataTypeName.cs | 139 +++++++++--------- src/Npgsql/PostgresTypes/PostgresType.cs | 2 +- .../PostgresTypes/PostgresUnknownType.cs | 6 +- test/Npgsql.Tests/DataTypeNameTests.cs | 39 +++++ 5 files changed, 116 insertions(+), 76 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs index aca4144a09..5c700ac7e3 100644 --- a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs +++ b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs @@ -241,9 +241,9 @@ internal void ProcessTypes() ByFullName[type.DataTypeName.Value] = type; // If more than one type exists with the same partial name, we place a null value. // This allows us to detect this case later and force the user to use full names only. - ByName[type.InternalName] = ByName.ContainsKey(type.InternalName) - ? null - : type; + var typeInternalName = type.InternalName; + if (!ByName.TryAdd(typeInternalName, type)) + ByName[typeInternalName] = null; switch (type) { diff --git a/src/Npgsql/Internal/Postgres/DataTypeName.cs b/src/Npgsql/Internal/Postgres/DataTypeName.cs index 616881f385..e1b8225911 100644 --- a/src/Npgsql/Internal/Postgres/DataTypeName.cs +++ b/src/Npgsql/Internal/Postgres/DataTypeName.cs @@ -27,7 +27,7 @@ namespace Npgsql.Internal.Postgres; if (!validated) { var schemaEndIndex = fullyQualifiedDataTypeName.IndexOf('.'); - if (schemaEndIndex == -1) + if (schemaEndIndex is -1 or 0) throw new ArgumentException("Given value does not contain a schema.", nameof(fullyQualifiedDataTypeName)); // Friendly array syntax is the only fully qualified name quirk that's allowed by postgres (see FromDisplayName). @@ -86,108 +86,108 @@ public DataTypeName ToArrayName() if (unqualifiedNameSpan.StartsWith("_".AsSpan(), StringComparison.Ordinal)) return this; - var unqualifiedName = unqualifiedNameSpan.ToString(); - if (unqualifiedName.Length + "_".Length > NAMEDATALEN) - unqualifiedName = unqualifiedName.Substring(0, NAMEDATALEN - "_".Length); + if (unqualifiedNameSpan.Length + "_".Length > NAMEDATALEN) + unqualifiedNameSpan = unqualifiedNameSpan.Slice(0, NAMEDATALEN - "_".Length); - return new(Schema + "._" + unqualifiedName); + return new(string.Concat(Schema, "._", unqualifiedNameSpan)); } // Static transform as defined by https://www.postgresql.org/docs/current/sql-createtype.html#SQL-CREATETYPE-RANGE // Manual testing on PG confirmed it's only the first occurence of 'range' that gets replaced. public DataTypeName ToDefaultMultirangeName() { - var unqualifiedNameSpan = UnqualifiedNameSpan; - if (UnqualifiedNameSpan.IndexOf("multirange".AsSpan(), StringComparison.Ordinal) != -1) + var nameSpan = UnqualifiedNameSpan; + if (nameSpan.IndexOf("multirange".AsSpan(), StringComparison.Ordinal) is not -1) return this; - var unqualifiedName = unqualifiedNameSpan.ToString(); - var rangeIndex = unqualifiedName.IndexOf("range", StringComparison.Ordinal); - if (rangeIndex != -1) + if (nameSpan.IndexOf("range", StringComparison.Ordinal) is var rangeIndex and not -1) { - var str = unqualifiedName.Substring(0, rangeIndex) + "multirange" + unqualifiedName.Substring(rangeIndex + "range".Length); - - return new($"{Schema}." + (unqualifiedName.Length + "multi".Length > NAMEDATALEN - ? str.Substring(0, NAMEDATALEN - "multi".Length) - : str)); + nameSpan = string.Concat(nameSpan.Slice(0, rangeIndex), "multirange", nameSpan.Slice(rangeIndex + "range".Length)); + return new(string.Concat(SchemaSpan, ".", + nameSpan.Length > NAMEDATALEN ? nameSpan.Slice(0, NAMEDATALEN) : nameSpan)); } - return new($"{Schema}." + (unqualifiedName.Length + "multi".Length > NAMEDATALEN - ? unqualifiedName.Substring(0, NAMEDATALEN - "_multirange".Length) + "_multirange" - : unqualifiedName + "_multirange")); + if (nameSpan.Length + "_multirange".Length > NAMEDATALEN) + nameSpan = nameSpan.Slice(0, NAMEDATALEN - "_multirange".Length); + + return new(string.Concat(SchemaSpan, ".", nameSpan, "_multirange")); } // 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) { var displayNameSpan = displayName.AsSpan().Trim(); - // If we have a schema we're done, Postgres doesn't do display name conversions on fully qualified names. - // There is one exception and that's array syntax, which is always resolvable in both ways, while we want the canonical name. var schemaEndIndex = displayNameSpan.IndexOf('.'); - if (schemaEndIndex is not -1 && - string.IsNullOrEmpty(schema) && - !displayNameSpan.Slice(schemaEndIndex).StartsWith("_".AsSpan(), StringComparison.Ordinal) && - !displayNameSpan.EndsWith("[]".AsSpan(), StringComparison.Ordinal)) - return new(displayName); - - // First we strip the schema to get the type name. - if (schemaEndIndex is not -1 && - string.IsNullOrEmpty(schema)) + ReadOnlySpan schemaSpan; + if (schemaEndIndex is not -1 && !assumeUnqualified) { - schema = displayNameSpan.Slice(0, schemaEndIndex).ToString(); + 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 ? "pg_catalog" : schema.AsSpan(); + } // Then we strip either of the two valid array representations to get the base type name (with or without facets). var isArray = false; - if (displayNameSpan.StartsWith("_".AsSpan())) + if (displayNameSpan.StartsWith("_", StringComparison.Ordinal)) { isArray = true; displayNameSpan = displayNameSpan.Slice(1); } - else if (displayNameSpan.EndsWith("[]".AsSpan())) + else if (displayNameSpan.EndsWith("[]", StringComparison.Ordinal)) { isArray = true; displayNameSpan = displayNameSpan.Slice(0, displayNameSpan.Length - 2); } - string mapped; - if (schemaEndIndex is -1) + if (schemaEndIndex is not -1) { - // Finally we strip the facet info. - var parenIndex = displayNameSpan.IndexOf('('); - if (parenIndex > -1) - displayNameSpan = displayNameSpan.Slice(0, parenIndex); - - // Map any aliases to the internal type name. - mapped = displayNameSpan.ToString() switch - { - "boolean" => "bool", - "character" => "bpchar", - "decimal" => "numeric", - "real" => "float4", - "double precision" => "float8", - "smallint" => "int2", - "integer" => "int4", - "bigint" => "int8", - "time without time zone" => "time", - "timestamp without time zone" => "timestamp", - "time with time zone" => "timetz", - "timestamp with time zone" => "timestamptz", - "bit varying" => "varbit", - "character varying" => "varchar", - var value => value - }; + // If we have a schema we're done, Postgres doesn't do display name conversions on fully qualified names. + // There is one exception and that's array syntax, which is always resolvable in both ways, while we want the canonical name. + return !isArray + ? new(displayName.Length == schemaEndIndex + displayNameSpan.Length + ? displayName + : string.Concat(schemaSpan, ".", displayNameSpan)) + : new(string.Concat(schemaSpan, ".", "_", displayNameSpan)); } - else + + // Finally we strip the facet info. + var parenIndex = displayNameSpan.IndexOf('('); + if (parenIndex > -1) + displayNameSpan = displayNameSpan.Slice(0, parenIndex); + + // Map any aliases to the internal type name. + var mapped = displayNameSpan switch { - // If we had a schema originally we stop here, see comment at schemaEndIndex. - mapped = displayNameSpan.ToString(); - } + "boolean" => "bool", + "character" => "bpchar", + "decimal" => "numeric", + "real" => "float4", + "double precision" => "float8", + "smallint" => "int2", + "integer" => "int4", + "bigint" => "int8", + "time without time zone" => "time", + "timestamp without time zone" => "timestamp", + "time with time zone" => "timetz", + "timestamp with time zone" => "timestamptz", + "bit varying" => "varbit", + "character varying" => "varchar", + var value => value + }; - return new((schema ?? "pg_catalog") + "." + (isArray ? "_" : "") + mapped); + return new(string.Concat(schemaSpan, ".", isArray ? "_" : "", mapped)); } // The type names stored in a DataTypeName are usually the actual typname from the pg_type column. @@ -197,8 +197,8 @@ public static DataTypeName FromDisplayName(string displayName, string? schema = // 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) { - var isArray = unqualifiedName.IndexOf('_') == 0; - var baseTypeName = isArray ? unqualifiedName.Slice(1).ToString() : unqualifiedName.ToString(); + var isArray = unqualifiedName.IndexOf('_') is 0; + var baseTypeName = isArray ? unqualifiedName.Slice(1) : unqualifiedName; var mappedBaseType = baseTypeName switch { @@ -216,13 +216,12 @@ static string ToDisplayName(ReadOnlySpan unqualifiedName) "timestamptz" => "timestamp with time zone", "varbit" => "bit varying", "varchar" => "character varying", - _ => baseTypeName + _ => null }; - if (isArray) - return mappedBaseType + "[]"; - - return mappedBaseType; + return isArray + ? string.Concat(mappedBaseType ?? baseTypeName, "[]") + : mappedBaseType ?? baseTypeName.ToString(); } internal static bool IsFullyQualified(ReadOnlySpan dataTypeName) => dataTypeName.Contains(".".AsSpan(), StringComparison.Ordinal); diff --git a/src/Npgsql/PostgresTypes/PostgresType.cs b/src/Npgsql/PostgresTypes/PostgresType.cs index 1182588c8c..842d1f3eea 100644 --- a/src/Npgsql/PostgresTypes/PostgresType.cs +++ b/src/Npgsql/PostgresTypes/PostgresType.cs @@ -24,7 +24,7 @@ public abstract class PostgresType /// The data type's OID. private protected PostgresType(string ns, string name, uint oid) { - DataTypeName = DataTypeName.FromDisplayName(name, ns); + DataTypeName = DataTypeName.FromDisplayName(name, ns, assumeUnqualified: true); OID = oid; FullName = Namespace + "." + Name; } diff --git a/src/Npgsql/PostgresTypes/PostgresUnknownType.cs b/src/Npgsql/PostgresTypes/PostgresUnknownType.cs index 2955295000..d7cfc983e9 100644 --- a/src/Npgsql/PostgresTypes/PostgresUnknownType.cs +++ b/src/Npgsql/PostgresTypes/PostgresUnknownType.cs @@ -1,4 +1,6 @@ -namespace Npgsql.PostgresTypes; +using Npgsql.Internal.Postgres; + +namespace Npgsql.PostgresTypes; /// /// Represents a PostgreSQL data type that isn't known to Npgsql and cannot be handled. @@ -10,5 +12,5 @@ public sealed class UnknownBackendType : PostgresType /// /// Constructs a the unknown backend type. /// - UnknownBackendType() : base("", "", 0) { } + UnknownBackendType() : base(DataTypeName.Unspecified,0) { } } diff --git a/test/Npgsql.Tests/DataTypeNameTests.cs b/test/Npgsql.Tests/DataTypeNameTests.cs index 7ca6c669ce..5c64baa607 100644 --- a/test/Npgsql.Tests/DataTypeNameTests.cs +++ b/test/Npgsql.Tests/DataTypeNameTests.cs @@ -23,4 +23,43 @@ public void TooLongDataTypeName() var exception = Assert.Throws(() => new DataTypeName(fullyQualifiedDataTypeName)); Assert.That(exception!.Message, Does.EndWith($": public.{new string('a', DataTypeName.NAMEDATALEN)}")); } + + [TestCase("public.name", ExpectedResult = "public._name")] + [TestCase("public._name", ExpectedResult = "public._name")] + [TestCase("public.zzzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa123", ExpectedResult = "public._zzzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa12")] + public string ToArrayName(string name) + => new DataTypeName(name).ToArrayName(); + + [TestCase("public.multirange", ExpectedResult = "public.multirange")] + [TestCase("public.abcmultirange123", ExpectedResult = "public.abcmultirange123")] + [TestCase("public.multiRANGE", ExpectedResult = "public.multiRANGE_multirange")] + public string ToDefaultMultirangeNameHasMultiRange(string name) + => new DataTypeName(name).ToDefaultMultirangeName(); + + [TestCase("public.range", ExpectedResult = "public.multirange")] + [TestCase("public.abcrange123", ExpectedResult = "public.abcmultirange123")] + [TestCase("public.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarange", ExpectedResult = "public.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaamultirange")] // Replace goes to max length + [TestCase("public.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarange1", ExpectedResult = "public.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaamultir")] // Replace goes over max length + [TestCase("public.RANGE", ExpectedResult = "public.RANGE_multirange")] + public string ToDefaultMultirangeNameHasRange(string name) + => new DataTypeName(name).ToDefaultMultirangeName(); + + [TestCase("public.name", null, ExpectedResult = "public.name")] + [TestCase("public._name", null, ExpectedResult = "public._name")] + [TestCase("public.name[]", null, ExpectedResult = "public._name")] + [TestCase("public.integer", null, ExpectedResult = "public.integer")] + [TestCase("name", null, ExpectedResult = "pg_catalog.name")] + [TestCase("_name", null, ExpectedResult = "pg_catalog._name")] + [TestCase("name[]", null, ExpectedResult = "pg_catalog._name")] + [TestCase("character varying", null, ExpectedResult = "pg_catalog.varchar")] + [TestCase("decimal(facet_name)", null, ExpectedResult = "pg_catalog.numeric")] + [TestCase("name", "public", ExpectedResult = "public.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(" public.name ", null, ExpectedResult = "public.name")] + public string FromDisplayName(string name, string? schema) + => DataTypeName.FromDisplayName(name, schema).Value; } From cc310e6191abf81764e34682eae6550edc40d89c Mon Sep 17 00:00:00 2001 From: Sergiusz <38229504+KeterSCP@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:04:14 +0100 Subject: [PATCH 099/155] Use actual version of the Npgsql for ActivitySource (#6277) --- src/Npgsql/NpgsqlActivitySource.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index 4493bb272a..e40ae5a9bd 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -4,12 +4,13 @@ using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Reflection; namespace Npgsql; static class NpgsqlActivitySource { - static readonly ActivitySource Source = new("Npgsql", "0.2.0"); + static readonly ActivitySource Source = new("Npgsql", GetLibraryVersion()); internal static bool IsEnabled => Source.HasListeners(); @@ -143,4 +144,9 @@ internal static void SetException(Activity activity, Exception ex, bool escaped activity.SetStatus(ActivityStatusCode.Error, statusDescription); activity.Dispose(); } + + static string GetLibraryVersion() + => typeof(NpgsqlDataSource).Assembly + .GetCustomAttribute()? + .InformationalVersion ?? "UNKNOWN"; } From b38cc40fab9d123b8551355bf528267ec2c5f7e0 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 30 Oct 2025 10:08:02 +0100 Subject: [PATCH 100/155] Remove manual CodeQL workflow As it now runs integrated with github --- .github/workflows/codeql-analysis.yml | 90 --------------------------- 1 file changed, 90 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 666e98fc85..0000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,90 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: - - main - - 'hotfix/**' - - 'release/**' - pull_request: - # The branches below must be a subset of the branches above - branches: - - main - - 'hotfix/**' - - 'release/**' - schedule: - - cron: '21 0 * * 4' - -# Cancel previous PR branch commits (head_ref is only defined on PRs) -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v5.0.0 - - - name: Build - run: dotnet build -c Release - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 From 492a56e1ecff115a083c00b621cf365c6bde6113 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 30 Oct 2025 13:18:20 +0100 Subject: [PATCH 101/155] Stop testing PostGIS on Windows in CI (#6275) Because of installation reliability issues --- .github/workflows/build.yml | 28 ++++++---------------------- test/Npgsql.Tests/TestUtil.cs | 13 ++++--------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff7a6581f7..e474e94763 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,12 @@ jobs: is_release: ${{ steps.analyze_tag.outputs.is_release }} is_prerelease: ${{ steps.analyze_tag.outputs.is_prerelease }} + # Installing PostGIS on Windows is complicated/unreliable, so we don't test on it. + # The NPGSQL_TEST_POSTGIS environment variable ensures that if PostGIS isn't installed, + # the PostGIS tests fail and therefore fail the build. + env: + NPGSQL_TEST_POSTGIS: ${{ !startsWith(matrix.os, 'windows') }} + steps: - name: Checkout uses: actions/checkout@v5 @@ -165,28 +171,6 @@ jobs: # Match Npgsql CI Docker image and stash one level up cp $GITHUB_WORKSPACE/.build/{server.crt,server.key,ca.crt} pgsql - # Find OSGEO version number - OSGEO_VERSION=$(\ - curl -Ls https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }} | - sed -n 's/.*>postgis-bundle-pg${{ matrix.pg_major }}-\(${{ env.postgis_version }}.[0-9]*.[0-9]*\)x64.zip<.*/\1/p' | - tail -n 1) - if [ -z "$OSGEO_VERSION" ]; then - OSGEO_VERSION=$(\ - curl -Ls https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }}/archive | - sed -n 's/.*>postgis-bundle-pg${{ matrix.pg_major }}-\(${{ env.postgis_version }}.[0-9]*.[0-9]*\)x64.zip<.*/\1/p' | - tail -n 1) - POSTGIS_PATH="archive/" - else - POSTGIS_PATH="" - fi - - # Install PostGIS - echo "Installing PostGIS (version: ${OSGEO_VERSION})" - POSTGIS_FILE="postgis-bundle-pg${{ matrix.pg_major }}-${OSGEO_VERSION}x64" - curl -o postgis.zip -L https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }}/${POSTGIS_PATH}${POSTGIS_FILE}.zip - unzip postgis.zip -d postgis - cp -a postgis/$POSTGIS_FILE/. pgsql/ - # Start PostgreSQL pgsql/bin/initdb -D pgsql/PGDATA -E UTF8 -U postgres SOCKET_DIR=$(echo "$LOCALAPPDATA\Temp" | sed 's|\\|/|g') diff --git a/test/Npgsql.Tests/TestUtil.cs b/test/Npgsql.Tests/TestUtil.cs index fc0b5404d8..85141c1cfa 100644 --- a/test/Npgsql.Tests/TestUtil.cs +++ b/test/Npgsql.Tests/TestUtil.cs @@ -100,9 +100,6 @@ public static async Task IgnoreOnRedshift(NpgsqlConnection conn, string? ignoreT } } - public static async Task IsPgPrerelease(NpgsqlConnection conn) - => ((string) (await conn.ExecuteScalarAsync("SELECT version()"))!).Contains("beta"); - public static void EnsureExtension(NpgsqlConnection conn, string extension, string? minVersion = null) => EnsureExtension(conn, extension, minVersion, async: false).GetAwaiter().GetResult(); @@ -168,21 +165,19 @@ static async Task IgnoreIfFeatureNotSupported(NpgsqlConnection conn, string test public static async Task EnsurePostgis(NpgsqlConnection conn) { - var isPreRelease = await IsPgPrerelease(conn); try { await EnsureExtensionAsync(conn, "postgis"); } - catch (PostgresException e) when (e.SqlState == PostgresErrorCodes.UndefinedFile) + catch (PostgresException) { - // PostGIS packages aren't available for PostgreSQL prereleases - if (isPreRelease) + if (Environment.GetEnvironmentVariable("NPGSQL_TEST_POSTGIS")?.ToLower(CultureInfo.InvariantCulture) is "1" or "true") { - Assert.Ignore($"PostGIS could not be installed, but PostgreSQL is prerelease ({conn.ServerVersion}), ignoring test suite."); + throw; } else { - throw; + Assert.Ignore($"PostGIS isn't installed, skipping tests"); } } } From 332ce0b2ffd7af66d9bd02ad4fdc20ba1dba9f00 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 30 Oct 2025 17:04:00 +0300 Subject: [PATCH 102/155] Upgrade to postgres 18 for CI (#6223) Co-authored-by: Shay Rojansky --- .github/workflows/build.yml | 24 ++++++++++++------------ global.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e474e94763..1eff7639e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,31 +29,31 @@ jobs: fail-fast: false matrix: os: [ubuntu-24.04] - pg_major: [17, 16, 15, 14, 13] + pg_major: [18, 17, 16, 15, 14] config: [Release] - test_tfm: [net9.0] + test_tfm: [net10.0] include: - os: ubuntu-24.04 - pg_major: 17 + pg_major: 18 config: Debug - test_tfm: net9.0 + test_tfm: net10.0 - os: macos-15 pg_major: 16 config: Release - test_tfm: net9.0 + test_tfm: net10.0 - os: windows-2022 - pg_major: 17 - config: Release - test_tfm: net9.0 - - os: ubuntu-24.04 - pg_major: 17 + pg_major: 18 config: Release - test_tfm: net8.0 + test_tfm: net10.0 - os: ubuntu-24.04 pg_major: 18 config: Release test_tfm: net8.0 - pg_prerelease: 'PG Prerelease' +# - os: ubuntu-24.04 +# pg_major: 19 +# config: Release +# test_tfm: net10.0 +# pg_prerelease: 'PG Prerelease' outputs: is_release: ${{ steps.analyze_tag.outputs.is_release }} diff --git a/global.json b/global.json index 838198f254..a08ab85427 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100-rc.1.25451.107", + "version": "10.0.100-rc.2.25502.107", "rollForward": "latestMajor", "allowPrerelease": false } From 7d0e3e140462cfc2a3647451f4035c54f4f2146d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:34:31 +0100 Subject: [PATCH 103/155] Bump BenchmarkDotNet from 0.15.4 to 0.15.5 (#6279) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 836b907a56..25efb12069 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + From aa3e200f7fa41443d72a1d41f6d2bfbf11a88f09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:37:09 +0000 Subject: [PATCH 104/155] Bump NUnit.Analyzers from 4.10.0 to 4.11.0 (#6280) --- Directory.Packages.props | 2 +- test/Npgsql.Tests/Npgsql.Tests.csproj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 25efb12069..5355861f54 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 1c300f8215..3aeb70bc28 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -21,6 +21,7 @@ true + $(NoWarn);NUnit1001 $(NoWarn);NPG9001 $(NoWarn);NPG9002 From ebf251dd127883fddc85ea0abdc1b1ab6cea328b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:12:43 +0100 Subject: [PATCH 105/155] Bump NUnit.Analyzers from 4.11.0 to 4.11.1 (#6281) --- updated-dependencies: - dependency-name: NUnit.Analyzers dependency-version: 4.11.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5355861f54..6a99e09cfc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + From 24b9e401b066a4e5d064d13eabe8bda5b7555722 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sun, 2 Nov 2025 14:12:49 +0100 Subject: [PATCH 106/155] Move json dom mappings to the NativeAOT compatible resolver (#6271) Closes #6052 --- .../JsonDynamicTypeInfoResolverFactory.cs | 23 ------- .../JsonTypeInfoResolverFactory.cs | 19 ++++++ test/Npgsql.Tests/Types/JsonDynamicTests.cs | 59 ------------------ test/Npgsql.Tests/Types/JsonTests.cs | 60 ++++++++++++++++++- 4 files changed, 77 insertions(+), 84 deletions(-) diff --git a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs index 04e3aa3313..696aac8efb 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonDynamicTypeInfoResolverFactory.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; using Npgsql.Internal.Converters; using Npgsql.Internal.Postgres; @@ -66,20 +65,6 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, // We do GetTypeInfo calls directly so we need a resolver. serializerOptions.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); - // These live in the RUC/RDC part as JsonValues can contain any .NET type. - foreach (var dataTypeName in new[] { DataTypeNames.Jsonb, DataTypeNames.Json }) - { - var jsonb = dataTypeName == DataTypeNames.Jsonb; - mappings.AddType(dataTypeName, (options, mapping, _) => - mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); - mappings.AddType(dataTypeName, (options, mapping, _) => - mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); - mappings.AddType(dataTypeName, (options, mapping, _) => - mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); - mappings.AddType(dataTypeName, (options, mapping, _) => - mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); - } - AddUserMappings(jsonb: true, jsonbClrTypes); AddUserMappings(jsonb: false, jsonClrTypes); @@ -164,14 +149,6 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, if (baseMappings.Items.Count == 0) return mappings; - foreach (var dataTypeName in new[] { DataTypeNames.Jsonb, DataTypeNames.Json }) - { - mappings.AddArrayType(dataTypeName); - mappings.AddArrayType(dataTypeName); - mappings.AddArrayType(dataTypeName); - mappings.AddArrayType(dataTypeName); - } - var dynamicMappings = CreateCollection(baseMappings); foreach (var mapping in baseMappings.Items) dynamicMappings.AddArrayMapping(mapping.Type, mapping.DataTypeName); diff --git a/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs index 250e000022..f778bea186 100644 --- a/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/JsonTypeInfoResolverFactory.cs @@ -1,5 +1,6 @@ using System; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; using Npgsql.Internal.Converters; using Npgsql.Internal.Postgres; @@ -47,6 +48,15 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings, mappings.AddStructType(dataTypeName, (options, mapping, _) => mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); + + mappings.AddType(dataTypeName, (options, mapping, _) => + mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); + mappings.AddType(dataTypeName, (options, mapping, _) => + mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); + mappings.AddType(dataTypeName, (options, mapping, _) => + mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); + mappings.AddType(dataTypeName, (options, mapping, _) => + mapping.CreateInfo(options, new JsonConverter(jsonb, options.TextEncoding, serializerOptions))); } return mappings; @@ -63,6 +73,12 @@ sealed class BasicJsonTypeInfoResolver : IJsonTypeInfoResolver return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonDocumentConverter); if (type == typeof(JsonElement)) return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonElementConverter); + if (type == typeof(JsonObject)) + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonObjectConverter); + if (type == typeof(JsonArray)) + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonArrayConverter); + if (type == typeof(JsonValue)) + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.JsonValueConverter); return null; } } @@ -82,6 +98,9 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) { mappings.AddArrayType(dataTypeName); mappings.AddStructArrayType(dataTypeName); + mappings.AddArrayType(dataTypeName); + mappings.AddArrayType(dataTypeName); + mappings.AddArrayType(dataTypeName); } return mappings; diff --git a/test/Npgsql.Tests/Types/JsonDynamicTests.cs b/test/Npgsql.Tests/Types/JsonDynamicTests.cs index 59a3d24662..af282f82d0 100644 --- a/test/Npgsql.Tests/Types/JsonDynamicTests.cs +++ b/test/Npgsql.Tests/Types/JsonDynamicTests.cs @@ -1,6 +1,5 @@ using System; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading.Tasks; using Npgsql.Properties; @@ -15,64 +14,6 @@ namespace Npgsql.Tests.Types; [TestFixture(MultiplexingMode.Multiplexing, NpgsqlDbType.Jsonb)] public class JsonDynamicTests : MultiplexingTestBase { - [Test] - public Task Roundtrip_JsonObject() - => AssertType( - new JsonObject { ["Bar"] = 8 }, - IsJsonb ? """{"Bar": 8}""" : """{"Bar":8}""", - PostgresType, - NpgsqlDbType, - // By default we map JsonObject to jsonb - isDefaultForWriting: IsJsonb, - isDefaultForReading: false, - isNpgsqlDbTypeInferredFromClrType: false, - comparer: (x, y) => x.ToString() == y.ToString()); - - [Test] - public Task Roundtrip_JsonArray() - => AssertType( - new JsonArray { 1, 2, 3 }, - IsJsonb ? "[1, 2, 3]" : "[1,2,3]", - PostgresType, - NpgsqlDbType, - // By default we map JsonArray to jsonb - isDefaultForWriting: IsJsonb, - isDefaultForReading: false, - isNpgsqlDbTypeInferredFromClrType: false, - comparer: (x, y) => x.ToString() == y.ToString()); - - [Test] - [IssueLink("https://github.com/npgsql/npgsql/issues/4537")] - public async Task Write_jsonobject_array_without_npgsqldbtype() - { - // By default we map JsonObject to jsonb - if (!IsJsonb) - return; - - await using var conn = await OpenConnectionAsync(); - var tableName = await TestUtil.CreateTempTable(conn, "key SERIAL PRIMARY KEY, ingredients json[]"); - - await using var cmd = new NpgsqlCommand { Connection = conn }; - - var jsonObject1 = new JsonObject - { - { "name", "value1" }, - { "amount", 1 }, - { "unit", "ml" } - }; - - var jsonObject2 = new JsonObject - { - { "name", "value2" }, - { "amount", 2 }, - { "unit", "g" } - }; - - cmd.CommandText = $"INSERT INTO {tableName} (ingredients) VALUES (@p)"; - cmd.Parameters.Add(new("p", new[] { jsonObject1, jsonObject2 })); - await cmd.ExecuteNonQueryAsync(); - } - [Test] public async Task As_poco() => await AssertType( diff --git a/test/Npgsql.Tests/Types/JsonTests.cs b/test/Npgsql.Tests/Types/JsonTests.cs index e7a9b4576e..84b95389bb 100644 --- a/test/Npgsql.Tests/Types/JsonTests.cs +++ b/test/Npgsql.Tests/Types/JsonTests.cs @@ -1,10 +1,8 @@ using System; -using System.Data; using System.IO; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using System.Threading.Tasks; using NpgsqlTypes; using NUnit.Framework; @@ -168,6 +166,64 @@ public async Task Can_read_two_json_documents() Assert.That(car.RootElement.GetProperty("key").GetString(), Is.EqualTo("foo")); } + [Test] + public Task Roundtrip_JsonObject() + => AssertType( + new JsonObject { ["Bar"] = 8 }, + IsJsonb ? """{"Bar": 8}""" : """{"Bar":8}""", + PostgresType, + NpgsqlDbType, + // By default we map JsonObject to jsonb + isDefaultForWriting: IsJsonb, + isDefaultForReading: false, + isNpgsqlDbTypeInferredFromClrType: false, + comparer: (x, y) => x.ToString() == y.ToString()); + + [Test] + public Task Roundtrip_JsonArray() + => AssertType( + new JsonArray { 1, 2, 3 }, + IsJsonb ? "[1, 2, 3]" : "[1,2,3]", + PostgresType, + NpgsqlDbType, + // By default we map JsonArray to jsonb + isDefaultForWriting: IsJsonb, + isDefaultForReading: false, + isNpgsqlDbTypeInferredFromClrType: false, + comparer: (x, y) => x.ToString() == y.ToString()); + + [Test] + [IssueLink("https://github.com/npgsql/npgsql/issues/4537")] + public async Task Write_jsonobject_array_without_npgsqldbtype() + { + // By default we map JsonObject to jsonb + if (!IsJsonb) + return; + + await using var conn = await OpenConnectionAsync(); + var tableName = await TestUtil.CreateTempTable(conn, "key SERIAL PRIMARY KEY, ingredients json[]"); + + await using var cmd = new NpgsqlCommand { Connection = conn }; + + var jsonObject1 = new JsonObject + { + { "name", "value1" }, + { "amount", 1 }, + { "unit", "ml" } + }; + + var jsonObject2 = new JsonObject + { + { "name", "value2" }, + { "amount", 2 }, + { "unit", "g" } + }; + + cmd.CommandText = $"INSERT INTO {tableName} (ingredients) VALUES (@p)"; + cmd.Parameters.Add(new("p", new[] { jsonObject1, jsonObject2 })); + await cmd.ExecuteNonQueryAsync(); + } + public JsonTests(MultiplexingMode multiplexingMode, NpgsqlDbType npgsqlDbType) : base(multiplexingMode) { From 5719dc735dac27b1428f32e659f1f6bc0ebfa42d Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 3 Nov 2025 14:15:26 +0100 Subject: [PATCH 107/155] Undo NoWarn --- test/Npgsql.Tests/Npgsql.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 3aeb70bc28..1c300f8215 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -21,7 +21,6 @@ true - $(NoWarn);NUnit1001 $(NoWarn);NPG9001 $(NoWarn);NPG9002 From c3e2fc2026a6272fd1ad711b1d31160c7131f21b Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 3 Nov 2025 14:24:45 +0100 Subject: [PATCH 108/155] Add SupportsReading plumbing (#5472) --- src/Npgsql/Internal/AdoSerializerHelpers.cs | 4 +++ src/Npgsql/Internal/PgTypeInfo.cs | 7 +++++ .../AdoTypeInfoResolverFactory.cs | 16 +++++----- .../NetworkTypeInfoResolverFactory.cs | 9 ++---- src/Npgsql/Internal/TypeInfoMapping.cs | 31 ++++++++++--------- src/Npgsql/NpgsqlParameter.cs | 3 -- test/Npgsql.Tests/Types/ByteaTests.cs | 14 +++++++++ 7 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/Npgsql/Internal/AdoSerializerHelpers.cs b/src/Npgsql/Internal/AdoSerializerHelpers.cs index 177114f78d..21010b3f99 100644 --- a/src/Npgsql/Internal/AdoSerializerHelpers.cs +++ b/src/Npgsql/Internal/AdoSerializerHelpers.cs @@ -15,6 +15,8 @@ public static PgTypeInfo GetTypeInfoForReading(Type type, PgTypeId pgTypeId, PgS try { typeInfo = options.GetTypeInfoInternal(type, pgTypeId); + if (typeInfo is { SupportsReading: false }) + typeInfo = null; } catch (Exception ex) { @@ -41,6 +43,8 @@ public static PgTypeInfo GetTypeInfoForWriting(Type? type, PgTypeId? pgTypeId, P try { typeInfo = options.GetTypeInfoInternal(type, pgTypeId); + if (typeInfo is { SupportsWriting: false }) + typeInfo = null; } catch (Exception ex) { diff --git a/src/Npgsql/Internal/PgTypeInfo.cs b/src/Npgsql/Internal/PgTypeInfo.cs index 836cd941b8..93b90b3a70 100644 --- a/src/Npgsql/Internal/PgTypeInfo.cs +++ b/src/Npgsql/Internal/PgTypeInfo.cs @@ -21,6 +21,7 @@ public class PgTypeInfo Options = options; IsBoxing = unboxedType is not null; Type = unboxedType ?? type; + SupportsReading = GetDefaultSupportsReading(type, unboxedType); SupportsWriting = true; } @@ -54,6 +55,7 @@ private protected PgTypeInfo(PgSerializerOptions options, Type type, PgConverter public Type Type { get; } public PgSerializerOptions Options { get; } + public bool SupportsReading { get; init; } public bool SupportsWriting { get; init; } public DataFormat? PreferredFormat { get; init; } @@ -240,6 +242,11 @@ DataFormat ResolveFormat(PgConverter converter, out BufferRequirements bufferReq return default; } } + + // We assume a boxing type info does not support reading as the converter won't be able to produce the derived type statically. + // Cases like Array converters unboxing to int[], int[,] etc. are the exception and the reason why SupportsReading is a settable property. + internal static bool GetDefaultSupportsReading(Type type, Type? unboxedType) + => unboxedType is null || unboxedType == type; } public sealed class PgResolverTypeInfo( diff --git a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs index db350e2fc9..8db547315f 100644 --- a/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/AdoTypeInfoResolverFactory.cs @@ -91,10 +91,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) }); //Special mappings, these have no corresponding array mapping. mappings.AddType(DataTypeNames.Text, - static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); mappings.AddStructType(DataTypeNames.Text, - static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); // Alternative text types @@ -118,10 +118,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) }); //Special mappings, these have no corresponding array mapping. mappings.AddType(dataTypeName, - static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); mappings.AddStructType(dataTypeName, - static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); } @@ -142,10 +142,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) }); //Special mappings, these have no corresponding array mapping. mappings.AddType(DataTypeNames.Jsonb, - static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonbVersion, new TextReaderTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonbVersion, new TextReaderTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); mappings.AddStructType(DataTypeNames.Jsonb, - static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonbVersion, new GetCharsTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonbVersion, new GetCharsTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); // Jsonpath @@ -154,10 +154,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonpathVersion, new StringTextConverter(options.TextEncoding))), isDefault: true); //Special mappings, these have no corresponding array mapping. mappings.AddType(DataTypeNames.Jsonpath, - static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonpathVersion, new TextReaderTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonpathVersion, new TextReaderTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); mappings.AddStructType(DataTypeNames.Jsonpath, - static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonpathVersion, new GetCharsTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text), + static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter(jsonpathVersion, new GetCharsTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false), MatchRequirement.DataTypeName); // Bytea diff --git a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs index 5eb072c1c9..6a2af4453f 100644 --- a/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs +++ b/src/Npgsql/Internal/ResolverFactories/NetworkTypeInfoResolverFactory.cs @@ -31,14 +31,9 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) // inet // There are certain IPAddress values like Loopback or Any that return a *private* derived type (see https://github.com/dotnet/runtime/issues/27870). - // However we still need to be able to resolve some typed converter for those values. - // We do so by returning a boxing info when we deal with a derived type, as a result we don't need an exact typed converter. - // For arrays users can't actually reference the private type so we'll only see some version of ArrayType. - // For reads we'll only see the public type so we never surface an InvalidCastException trying to cast IPAddress to ReadOnlyIPAddress. - // Finally we add a custom predicate to be able to match any type which values are assignable to IPAddress. mappings.AddType(DataTypeNames.Inet, - static (options, mapping, _) => new PgTypeInfo(options, new IPAddressConverter(), - new DataTypeName(mapping.DataTypeName), unboxedType: mapping.Type == typeof(IPAddress) ? null : mapping.Type), + static (options, mapping, _) => new PgTypeInfo(options, new IPAddressConverter(), new DataTypeName(mapping.DataTypeName), + unboxedType: mapping.Type != typeof(IPAddress) ? mapping.Type : null), mapping => mapping with { MatchRequirement = MatchRequirement.Single, diff --git a/src/Npgsql/Internal/TypeInfoMapping.cs b/src/Npgsql/Internal/TypeInfoMapping.cs index c8439de6ac..64b14dff73 100644 --- a/src/Npgsql/Internal/TypeInfoMapping.cs +++ b/src/Npgsql/Internal/TypeInfoMapping.cs @@ -72,7 +72,7 @@ public readonly struct TypeInfoMapping(Type type, string dataTypeName, TypeInfoF public bool TypeEquals(Type type) => TypeMatchPredicate?.Invoke(type) ?? Type == type; - private bool DataTypeNameEqualsCore(string dataTypeName) + bool DataTypeNameEqualsCore(string dataTypeName) { var span = DataTypeName.AsSpan(); return Postgres.DataTypeName.IsFullyQualified(span) @@ -196,7 +196,7 @@ TypeInfoMapping GetMapping(Type type, string dataTypeName) => TryGetMapping(type, dataTypeName, out var info) ? info : throw new InvalidOperationException($"Could not find mapping for {type} <-> {dataTypeName}"); // Helper to eliminate generic display class duplication. - static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func mapper, bool copyPreferredFormat = false, bool supportsWriting = true) + static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func mapper, bool copyPreferredFormat = false, bool? supportsReading = null, bool? supportsWriting = null) => (options, mapping, requiresDataTypeName) => { var resolvedInnerMapping = innerMapping; @@ -206,18 +206,20 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i var innerInfo = innerMapping.Factory(options, resolvedInnerMapping, requiresDataTypeName); var converter = mapper(mapping, innerInfo); var preferredFormat = copyPreferredFormat ? innerInfo.PreferredFormat : null; - var writingSupported = supportsWriting && innerInfo.SupportsWriting; var unboxedType = ComputeUnboxedType(defaultType: mappingType, converter.TypeToConvert, mapping.Type); + var readingSupported = innerInfo.SupportsReading && (supportsReading ?? PgTypeInfo.GetDefaultSupportsReading(converter.TypeToConvert, unboxedType)); + var writingSupported = innerInfo.SupportsWriting && (supportsWriting ?? true); return new PgTypeInfo(options, converter, options.GetCanonicalTypeId(new DataTypeName(mapping.DataTypeName)), unboxedType) { PreferredFormat = preferredFormat, + SupportsReading = readingSupported, SupportsWriting = writingSupported }; }; // Helper to eliminate generic display class duplication. - static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func mapper, bool copyPreferredFormat = false, bool supportsWriting = true) + static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func mapper, bool copyPreferredFormat = false, bool? supportsReading = null, bool? supportsWriting = null) => (options, mapping, requiresDataTypeName) => { var resolvedInnerMapping = innerMapping; @@ -227,8 +229,9 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i var innerInfo = (PgResolverTypeInfo)innerMapping.Factory(options, resolvedInnerMapping, requiresDataTypeName); var resolver = mapper(mapping, innerInfo); var preferredFormat = copyPreferredFormat ? innerInfo.PreferredFormat : null; - var writingSupported = supportsWriting && innerInfo.SupportsWriting; var unboxedType = ComputeUnboxedType(defaultType: mappingType, resolver.TypeToConvert, mapping.Type); + var readingSupported = innerInfo.SupportsReading && (supportsReading ?? PgTypeInfo.GetDefaultSupportsReading(resolver.TypeToConvert, unboxedType)); + var writingSupported = innerInfo.SupportsWriting && (supportsWriting ?? true); // We include the data type name if the inner info did so as well. // This way we can rely on its logic around resolvedDataTypeName, including when it ignores that flag. PgTypeId? pgTypeId = innerInfo.PgTypeId is not null @@ -237,6 +240,7 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i return new PgResolverTypeInfo(options, resolver, pgTypeId, unboxedType) { PreferredFormat = preferredFormat, + SupportsReading = readingSupported, SupportsWriting = writingSupported }; }; @@ -351,7 +355,7 @@ public void AddArrayType(TypeInfoMapping elementMapping, bool suppress void AddArrayType(TypeInfoMapping elementMapping, Type type, Func converter, Func? typeMatchPredicate = null, bool suppressObjectMapping = false) { - var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter)) + var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = typeMatchPredicate @@ -391,7 +395,7 @@ public void AddResolverArrayType(TypeInfoMapping elementMapping, bool void AddResolverArrayType(TypeInfoMapping elementMapping, Type type, Func converter, Func? typeMatchPredicate = null, bool suppressObjectMapping = false) { - var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter)) + var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = typeMatchPredicate @@ -483,12 +487,12 @@ void AddStructArrayType(TypeInfoMapping elementMapping, TypeInfoMapping nullable Func? typeMatchPredicate, Func? nullableTypeMatchPredicate, bool suppressObjectMapping) { var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName); - var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter)) + var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = typeMatchPredicate }; - var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter)) + var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter, supportsReading: true)) { MatchRequirement = arrayMapping.MatchRequirement, TypeMatchPredicate = nullableTypeMatchPredicate @@ -601,12 +605,12 @@ void AddResolverStructArrayType(TypeInfoMapping elementMapping, TypeInfoMapping { var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName); - var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter)) + var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = typeMatchPredicate }; - var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter)) + var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter, supportsReading: true)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = nullableTypeMatchPredicate @@ -654,7 +658,7 @@ void AddPolymorphicResolverArrayType(TypeInfoMapping elementMapping, Type type, { var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName); var mapping = new TypeInfoMapping(type, arrayDataTypeName, - CreateComposedFactory(typeof(Array), elementMapping, converter, supportsWriting: false)) + CreateComposedFactory(typeof(Array), elementMapping, converter, supportsReading: true, supportsWriting: false)) { MatchRequirement = elementMapping.MatchRequirement, TypeMatchPredicate = typeMatchPredicate @@ -811,8 +815,7 @@ public static PgTypeInfo CreateInfo(this TypeInfoMapping mapping, PgSerializerOp public static PgResolverTypeInfo CreateInfo(this TypeInfoMapping mapping, PgSerializerOptions options, PgConverterResolver resolver, bool includeDataTypeName) => new(options, resolver, includeDataTypeName ? new DataTypeName(mapping.DataTypeName) : null) { - PreferredFormat = null, - SupportsWriting = true + PreferredFormat = null }; /// diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index b1318d9b0a..ba2d840925 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -618,9 +618,6 @@ internal void Bind(out DataFormat format, out Size size, DataFormat? requiredFor if (TypeInfo is null) ThrowHelper.ThrowInvalidOperationException($"Missing type info, {nameof(ResolveTypeInfo)} needs to be called before {nameof(Bind)}."); - if (!TypeInfo.SupportsWriting) - ThrowHelper.ThrowNotSupportedException($"Cannot write values for parameters of type '{TypeInfo.Type}' and postgres type '{TypeInfo.Options.DatabaseInfo.GetDataTypeName(PgTypeId).DisplayName}'."); - // We might call this twice, once during validation and once during WriteBind, only compute things once. if (WriteSize is null) { diff --git a/test/Npgsql.Tests/Types/ByteaTests.cs b/test/Npgsql.Tests/Types/ByteaTests.cs index 9e8df154b0..216ae64cc4 100644 --- a/test/Npgsql.Tests/Types/ByteaTests.cs +++ b/test/Npgsql.Tests/Types/ByteaTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.IO; +using System.Net.Sockets; using System.Threading.Tasks; using NpgsqlTypes; using NUnit.Framework; @@ -284,6 +285,19 @@ public async Task Array_of_bytea() Assert.That(retVal[1], Is.EqualTo(inVal[1])); } + [Test] + public async Task InvalidCastException_unknown_stream_read() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT :p1", conn); + cmd.Parameters.AddWithValue("p1", NpgsqlDbType.Bytea, new byte[] { 1 }); + await using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + Assert.Throws(() => reader.GetFieldValue(0)); + } + } + sealed class NonSeekableStream(byte[] data) : MemoryStream(data) { public override bool CanSeek => false; From e77dee01733aad5e891a29ebadcde780a21aea56 Mon Sep 17 00:00:00 2001 From: Bruno Hoffmeister Date: Mon, 3 Nov 2025 10:46:43 -0300 Subject: [PATCH 109/155] Populate CommandText when NpgsqlBatchCommand is created (#6234) --- src/Npgsql/SqlQueryParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Npgsql/SqlQueryParser.cs b/src/Npgsql/SqlQueryParser.cs index 88728a34f7..c037a51342 100644 --- a/src/Npgsql/SqlQueryParser.cs +++ b/src/Npgsql/SqlQueryParser.cs @@ -506,6 +506,7 @@ void MoveToNextBatchCommand() else { batchCommand = new NpgsqlBatchCommand { _parameters = parameters }; + batchCommand.CommandText = sql; batchCommands.Add(batchCommand); } } From 530f0fb31a26a610fd0fdadee019dc5e97de84eb Mon Sep 17 00:00:00 2001 From: Bruce Bowyer-Smyth Date: Tue, 4 Nov 2025 00:40:33 +1000 Subject: [PATCH 110/155] Remove unsafe from WriteStringChunked (#5988) --- .../Internal/Converters/ArrayConverter.cs | 2 +- src/Npgsql/Internal/NpgsqlWriteBuffer.cs | 106 ++++-------------- src/Npgsql/Internal/PgWriter.cs | 3 +- src/Npgsql/PregeneratedMessages.cs | 2 +- test/Npgsql.Tests/WriteBufferTests.cs | 64 +++-------- 5 files changed, 41 insertions(+), 136 deletions(-) diff --git a/src/Npgsql/Internal/Converters/ArrayConverter.cs b/src/Npgsql/Internal/Converters/ArrayConverter.cs index 262f748651..2d6d443329 100644 --- a/src/Npgsql/Internal/Converters/ArrayConverter.cs +++ b/src/Npgsql/Internal/Converters/ArrayConverter.cs @@ -161,7 +161,7 @@ sealed class WriteState : MultiWriteState public required int[]? Lengths { get; init; } } - unsafe object ReadDimsAndCreateCollection(PgReader reader, int dimensions, out int lastDimLength) + object ReadDimsAndCreateCollection(PgReader reader, int dimensions, out int lastDimLength) { Debug.Assert(!reader.ShouldBuffer((sizeof(int) + sizeof(int)) * dimensions)); diff --git a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs index c768020718..0e176c2986 100644 --- a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs @@ -84,6 +84,8 @@ internal PgWriter GetWriter(NpgsqlDatabaseInfo typeCatalog, FlushMode flushMode bool _disposed; readonly PgWriter _pgWriter; + Span Span => Buffer.AsSpan(WritePosition, WriteSpaceLeft); + /// /// The minimum buffer size possible. /// @@ -329,46 +331,49 @@ static void ThrowNotSpaceLeft() => ThrowHelper.ThrowInvalidOperationException("There is not enough space left in the buffer."); public Task WriteString(string s, int byteLen, bool async, CancellationToken cancellationToken = default) - => WriteString(s, s.Length, byteLen, async, cancellationToken); - - public Task WriteString(string s, int charLen, int byteLen, bool async, CancellationToken cancellationToken = default) { if (byteLen <= WriteSpaceLeft) { - WriteString(s, charLen); + WriteString(s); return Task.CompletedTask; } - return WriteStringLong(this, async, s, charLen, byteLen, cancellationToken); + return WriteStringLong(this, async, s, byteLen, cancellationToken); - static async Task WriteStringLong(NpgsqlWriteBuffer buffer, bool async, string s, int charLen, int byteLen, CancellationToken cancellationToken) + static async Task WriteStringLong(NpgsqlWriteBuffer buffer, bool async, string s, int byteLen, CancellationToken cancellationToken) { Debug.Assert(byteLen > buffer.WriteSpaceLeft); if (byteLen <= buffer.Size) { // String can fit entirely in an empty buffer. Flush and retry rather than - // going into the partial writing flow below (which requires ToCharArray()) + // going into the partial writing flow below await buffer.Flush(async, cancellationToken).ConfigureAwait(false); - buffer.WriteString(s, charLen); + buffer.WriteString(s); } else { - var charPos = 0; - while (true) + var encoder = buffer._textEncoder; + encoder.Reset(); + var data = s.AsMemory(); + var minBufferSize = buffer.TextEncoding.GetMaxByteCount(1); + + bool completed; + do { - buffer.WriteStringChunked(s, charPos, charLen - charPos, true, out var charsUsed, out var completed); - if (completed) - break; - await buffer.Flush(async, cancellationToken).ConfigureAwait(false); - charPos += charsUsed; - } + if (buffer.WriteSpaceLeft < minBufferSize) + await buffer.Flush(async, cancellationToken).ConfigureAwait(false); + encoder.Convert(data.Span, buffer.Span, flush: data.Length * minBufferSize <= buffer.Span.Length, + out var charsUsed, out var bytesUsed, out completed); + data = data.Slice(charsUsed); + buffer.WritePosition += bytesUsed; + } while (!completed); } } } - public void WriteString(string s, int len = 0) + public void WriteString(string s) { Debug.Assert(TextEncoding.GetByteCount(s) <= WriteSpaceLeft); - WritePosition += TextEncoding.GetBytes(s, 0, len == 0 ? s.Length : len, Buffer, WritePosition); + WritePosition += TextEncoding.GetBytes(s, 0, s.Length, Buffer, WritePosition); } public void WriteBytes(ReadOnlySpan buf) @@ -421,30 +426,6 @@ static async Task WriteBytesLong(NpgsqlWriteBuffer buffer, bool async, ReadOnlyM } } - public async Task WriteStreamRaw(Stream stream, int count, bool async, CancellationToken cancellationToken = default) - { - while (count > 0) - { - if (WriteSpaceLeft == 0) - await Flush(async, cancellationToken).ConfigureAwait(false); - try - { - var read = async - ? await stream.ReadAsync(Buffer, WritePosition, Math.Min(WriteSpaceLeft, count), cancellationToken).ConfigureAwait(false) - : stream.Read(Buffer, WritePosition, Math.Min(WriteSpaceLeft, count)); - if (read == 0) - throw new EndOfStreamException(); - WritePosition += read; - count -= read; - } - catch (Exception e) - { - throw Connector.Break(new NpgsqlException("Exception while writing to stream", e)); - } - } - Debug.Assert(count == 0); - } - public void WriteNullTerminatedString(string s) { AssertASCIIOnly(s); @@ -463,47 +444,6 @@ public void WriteNullTerminatedString(byte[] s) #endregion - #region Write Complex - - internal void WriteStringChunked(char[] chars, int charIndex, int charCount, - bool flush, out int charsUsed, out bool completed) - { - if (WriteSpaceLeft < _textEncoder.GetByteCount(chars, charIndex, char.IsHighSurrogate(chars[charIndex]) ? 2 : 1, flush: false)) - { - charsUsed = 0; - completed = false; - return; - } - - _textEncoder.Convert(chars, charIndex, charCount, Buffer, WritePosition, WriteSpaceLeft, - flush, out charsUsed, out var bytesUsed, out completed); - WritePosition += bytesUsed; - } - - internal unsafe void WriteStringChunked(string s, int charIndex, int charCount, - bool flush, out int charsUsed, out bool completed) - { - int bytesUsed; - - fixed (char* sPtr = s) - fixed (byte* bufPtr = Buffer) - { - if (WriteSpaceLeft < _textEncoder.GetByteCount(sPtr + charIndex, char.IsHighSurrogate(*(sPtr + charIndex)) ? 2 : 1, flush: false)) - { - charsUsed = 0; - completed = false; - return; - } - - _textEncoder.Convert(sPtr + charIndex, charCount, bufPtr + WritePosition, WriteSpaceLeft, - flush, out charsUsed, out bytesUsed, out completed); - } - - WritePosition += bytesUsed; - } - - #endregion - #region Copy internal void StartCopyMode() diff --git a/src/Npgsql/Internal/PgWriter.cs b/src/Npgsql/Internal/PgWriter.cs index fecf4b7474..8e22a54d55 100644 --- a/src/Npgsql/Internal/PgWriter.cs +++ b/src/Npgsql/Internal/PgWriter.cs @@ -298,7 +298,8 @@ void Core(ReadOnlySpan data, Encoding encoding) if (ShouldFlush(minBufferSize)) Flush(); Ensure(minBufferSize); - encoder.Convert(data, Span, flush: data.Length <= Span.Length, out var charsUsed, out var bytesUsed, out completed); + encoder.Convert(data, Span, flush: data.Length * minBufferSize <= Span.Length, + out var charsUsed, out var bytesUsed, out completed); data = data.Slice(charsUsed); Advance(bytesUsed); } while (!completed); diff --git a/src/Npgsql/PregeneratedMessages.cs b/src/Npgsql/PregeneratedMessages.cs index 54c736b64c..4e6434e9c1 100644 --- a/src/Npgsql/PregeneratedMessages.cs +++ b/src/Npgsql/PregeneratedMessages.cs @@ -26,7 +26,7 @@ internal static byte[] Generate(NpgsqlWriteBuffer buf, string query) { NpgsqlWriteBuffer.AssertASCIIOnly(query); - var queryByteLen = Encoding.ASCII.GetByteCount(query); + var queryByteLen = buf.TextEncoding.GetByteCount(query); buf.WriteByte(FrontendMessageCode.Query); buf.WriteInt32(4 + // Message length (including self excluding code) diff --git a/test/Npgsql.Tests/WriteBufferTests.cs b/test/Npgsql.Tests/WriteBufferTests.cs index ff8d9413ce..53bf753dd6 100644 --- a/test/Npgsql.Tests/WriteBufferTests.cs +++ b/test/Npgsql.Tests/WriteBufferTests.cs @@ -33,20 +33,17 @@ public void GetWriter_Full_Buffer() } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/1275")] - public void Write_zero_characters() + public void Chunked_string_with_full_buffer() { // Fill up the buffer entirely WriteBuffer.WriteBytes(new byte[WriteBuffer.Size], 0, WriteBuffer.Size); Assert.That(WriteBuffer.WriteSpaceLeft, Is.Zero); - int charsUsed; - bool completed; - WriteBuffer.WriteStringChunked("hello", 0, 5, true, out charsUsed, out completed); - Assert.That(charsUsed, Is.Zero); - Assert.That(completed, Is.False); - WriteBuffer.WriteStringChunked("hello".ToCharArray(), 0, 5, true, out charsUsed, out completed); - Assert.That(charsUsed, Is.Zero); - Assert.That(completed, Is.False); + var data = new string('a', WriteBuffer.Size) + "hello"; + var byteLength = WriteBuffer.TextEncoding.GetByteCount(data); + WriteBuffer.WriteString(data, byteLength, false); + Assert.That(WriteBuffer.WritePosition, Is.EqualTo(5)); + Assert.That(WriteBuffer.Buffer.AsSpan(0, 5).ToArray(), Is.EqualTo(new byte[] { (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o' })); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/2849")] @@ -55,26 +52,11 @@ public void Chunked_string_encoding_fits() WriteBuffer.WriteBytes(new byte[WriteBuffer.Size - 1], 0, WriteBuffer.Size - 1); Assert.That(WriteBuffer.WriteSpaceLeft, Is.EqualTo(1)); - var charsUsed = 1; - var completed = true; // This unicode character is three bytes when encoded in UTF8 - Assert.That(() => WriteBuffer.WriteStringChunked("\uD55C", 0, 1, true, out charsUsed, out completed), Throws.Nothing); - Assert.That(charsUsed, Is.EqualTo(0)); - Assert.That(completed, Is.False); - } - - [Test, IssueLink("https://github.com/npgsql/npgsql/issues/2849")] - public void Chunked_byte_array_encoding_fits() - { - WriteBuffer.WriteBytes(new byte[WriteBuffer.Size - 1], 0, WriteBuffer.Size - 1); - Assert.That(WriteBuffer.WriteSpaceLeft, Is.EqualTo(1)); - - var charsUsed = 1; - var completed = true; - // This unicode character is three bytes when encoded in UTF8 - Assert.That(() => WriteBuffer.WriteStringChunked("\uD55C".ToCharArray(), 0, 1, true, out charsUsed, out completed), Throws.Nothing); - Assert.That(charsUsed, Is.EqualTo(0)); - Assert.That(completed, Is.False); + var data = "\uD55C" + new string('a', WriteBuffer.Size); + var byteLength = WriteBuffer.TextEncoding.GetByteCount(data); + WriteBuffer.WriteString(data, byteLength, false); + Assert.That(WriteBuffer.WritePosition, Is.EqualTo(3)); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3733")] @@ -83,28 +65,10 @@ public void Chunked_string_encoding_fits_with_surrogates() WriteBuffer.WriteBytes(new byte[WriteBuffer.Size - 1]); Assert.That(WriteBuffer.WriteSpaceLeft, Is.EqualTo(1)); - var charsUsed = 1; - var completed = true; - var cyclone = "🌀"; - - Assert.That(() => WriteBuffer.WriteStringChunked(cyclone, 0, cyclone.Length, true, out charsUsed, out completed), Throws.Nothing); - Assert.That(charsUsed, Is.EqualTo(0)); - Assert.That(completed, Is.False); - } - - [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3733")] - public void Chunked_char_array_encoding_fits_with_surrogates() - { - WriteBuffer.WriteBytes(new byte[WriteBuffer.Size - 1]); - Assert.That(WriteBuffer.WriteSpaceLeft, Is.EqualTo(1)); - - var charsUsed = 1; - var completed = true; - var cyclone = "🌀"; - - Assert.That(() => WriteBuffer.WriteStringChunked(cyclone.ToCharArray(), 0, cyclone.Length, true, out charsUsed, out completed), Throws.Nothing); - Assert.That(charsUsed, Is.EqualTo(0)); - Assert.That(completed, Is.False); + var cyclone = "🌀" + new string('a', WriteBuffer.Size); + var byteLength = WriteBuffer.TextEncoding.GetByteCount(cyclone); + WriteBuffer.WriteString(cyclone, byteLength, false); + Assert.That(WriteBuffer.WritePosition, Is.EqualTo(4)); } [SetUp] From 0ec29b34afedfcbc07870ead61d1f71706c0d5e8 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 3 Nov 2025 15:50:08 +0100 Subject: [PATCH 111/155] As the inputs are all expected to be well-formed and complete flush can be true --- src/Npgsql/Internal/NpgsqlWriteBuffer.cs | 3 +-- src/Npgsql/Internal/PgWriter.cs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs index 0e176c2986..ea0b4b265a 100644 --- a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs +++ b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs @@ -361,8 +361,7 @@ static async Task WriteStringLong(NpgsqlWriteBuffer buffer, bool async, string s { if (buffer.WriteSpaceLeft < minBufferSize) await buffer.Flush(async, cancellationToken).ConfigureAwait(false); - encoder.Convert(data.Span, buffer.Span, flush: data.Length * minBufferSize <= buffer.Span.Length, - out var charsUsed, out var bytesUsed, out completed); + encoder.Convert(data.Span, buffer.Span, flush: true, out var charsUsed, out var bytesUsed, out completed); data = data.Slice(charsUsed); buffer.WritePosition += bytesUsed; } while (!completed); diff --git a/src/Npgsql/Internal/PgWriter.cs b/src/Npgsql/Internal/PgWriter.cs index 8e22a54d55..2d08a38e53 100644 --- a/src/Npgsql/Internal/PgWriter.cs +++ b/src/Npgsql/Internal/PgWriter.cs @@ -298,8 +298,7 @@ void Core(ReadOnlySpan data, Encoding encoding) if (ShouldFlush(minBufferSize)) Flush(); Ensure(minBufferSize); - encoder.Convert(data, Span, flush: data.Length * minBufferSize <= Span.Length, - out var charsUsed, out var bytesUsed, out completed); + encoder.Convert(data, Span, flush: true, out var charsUsed, out var bytesUsed, out completed); data = data.Slice(charsUsed); Advance(bytesUsed); } while (!completed); @@ -335,7 +334,7 @@ async ValueTask Core(ReadOnlyMemory data, Encoding encoding, CancellationT if (ShouldFlush(minBufferSize)) await FlushAsync(cancellationToken).ConfigureAwait(false); Ensure(minBufferSize); - encoder.Convert(data.Span, Span, flush: data.Length <= Span.Length, out var charsUsed, out var bytesUsed, out completed); + encoder.Convert(data.Span, Span, flush: true, out var charsUsed, out var bytesUsed, out completed); data = data.Slice(charsUsed); Advance(bytesUsed); } while (!completed); From 9163444a38b293e72f1ba591a6b6e4ab7ba07cc6 Mon Sep 17 00:00:00 2001 From: Kirk Brauer Date: Mon, 3 Nov 2025 10:03:31 -0500 Subject: [PATCH 112/155] Cube support (#3867) Closes #698 --- .../Converters/Geometric/CubeConverter.cs | 87 ++++++ .../CubeTypeInfoResolverFactory.cs | 56 ++++ .../UnsupportedTypeInfoResolver.cs | 1 + src/Npgsql/NpgsqlDataSourceBuilder.cs | 4 +- src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 10 + src/Npgsql/NpgsqlTypes/NpgsqlCube.cs | 251 ++++++++++++++++ src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs | 8 + .../Properties/NpgsqlStrings.Designer.cs | 11 +- src/Npgsql/Properties/NpgsqlStrings.resx | 3 + src/Npgsql/PublicAPI.Unshipped.txt | 21 ++ test/Npgsql.Tests/Types/CubeTests.cs | 277 ++++++++++++++++++ 11 files changed, 727 insertions(+), 2 deletions(-) create mode 100644 src/Npgsql/Internal/Converters/Geometric/CubeConverter.cs create mode 100644 src/Npgsql/Internal/ResolverFactories/CubeTypeInfoResolverFactory.cs create mode 100644 src/Npgsql/NpgsqlTypes/NpgsqlCube.cs create mode 100644 test/Npgsql.Tests/Types/CubeTests.cs diff --git a/src/Npgsql/Internal/Converters/Geometric/CubeConverter.cs b/src/Npgsql/Internal/Converters/Geometric/CubeConverter.cs new file mode 100644 index 0000000000..05b539cf12 --- /dev/null +++ b/src/Npgsql/Internal/Converters/Geometric/CubeConverter.cs @@ -0,0 +1,87 @@ +using System.Threading; +using System.Threading.Tasks; +using NpgsqlTypes; + +// ReSharper disable once CheckNamespace +namespace Npgsql.Internal.Converters; + +sealed class CubeConverter : PgStreamingConverter +{ + const uint PointBit = 0x80000000; + const int DimMask = 0x7fffffff; + + public override NpgsqlCube Read(PgReader reader) + => Read(async: false, reader, CancellationToken.None).GetAwaiter().GetResult(); + + public override ValueTask ReadAsync(PgReader reader, CancellationToken cancellationToken = default) + => Read(async: true, reader, cancellationToken); + + async ValueTask Read(bool async, PgReader reader, CancellationToken cancellationToken) + { + if (reader.ShouldBuffer(sizeof(int))) + await reader.Buffer(async, sizeof(int), cancellationToken).ConfigureAwait(false); + + var header = reader.ReadInt32(); + var dim = header & DimMask; + var point = (header & PointBit) != 0; + + var lowerLeft = new double[dim]; + for (var i = 0; i < dim; i++) + { + if (reader.ShouldBuffer(sizeof(double))) + await reader.Buffer(async, sizeof(double), cancellationToken).ConfigureAwait(false); + lowerLeft[i] = reader.ReadDouble(); + } + + if (point) + return new NpgsqlCube(lowerLeft); + + var upperRight = new double[dim]; + for (var i = 0; i < dim; i++) + { + if (reader.ShouldBuffer(sizeof(double))) + await reader.Buffer(async, sizeof(double), cancellationToken).ConfigureAwait(false); + upperRight[i] = reader.ReadDouble(); + } + + return new NpgsqlCube(lowerLeft, upperRight); + } + + public override Size GetSize(SizeContext context, NpgsqlCube value, ref object? writeState) + => sizeof(int) + sizeof(double) * (value.IsPoint ? value.Dimensions : value.Dimensions * 2); + + public override void Write(PgWriter writer, NpgsqlCube value) + => Write(async: false, writer, value, CancellationToken.None).GetAwaiter().GetResult(); + + public override ValueTask WriteAsync(PgWriter writer, NpgsqlCube value, CancellationToken cancellationToken = default) + => Write(async: true, writer, value, cancellationToken); + + async ValueTask Write(bool async, PgWriter writer, NpgsqlCube value, CancellationToken cancellationToken) + { + if (writer.ShouldFlush(sizeof(int))) + await writer.Flush(async, cancellationToken).ConfigureAwait(false); + + var header = value.Dimensions; + if (value.IsPoint) + header |= 1 << 31; + + writer.WriteInt32(header); + + for (var i = 0; i < value.Dimensions; i++) + { + if (writer.ShouldFlush(sizeof(double))) + await writer.Flush(async, cancellationToken).ConfigureAwait(false); + writer.WriteDouble(value.LowerLeft[i]); + } + + if (value.IsPoint) + return; + + for (var i = 0; i < value.Dimensions; i++) + { + if (writer.ShouldFlush(sizeof(double))) + await writer.Flush(async, cancellationToken).ConfigureAwait(false); + writer.WriteDouble(value.UpperRight[i]); + } + } +} diff --git a/src/Npgsql/Internal/ResolverFactories/CubeTypeInfoResolverFactory.cs b/src/Npgsql/Internal/ResolverFactories/CubeTypeInfoResolverFactory.cs new file mode 100644 index 0000000000..90b872f458 --- /dev/null +++ b/src/Npgsql/Internal/ResolverFactories/CubeTypeInfoResolverFactory.cs @@ -0,0 +1,56 @@ +using System; +using Npgsql.Internal.Converters; +using Npgsql.Internal.Postgres; +using Npgsql.Properties; +using NpgsqlTypes; + +namespace Npgsql.Internal.ResolverFactories; + +sealed class CubeTypeInfoResolverFactory : PgTypeInfoResolverFactory +{ + const string CubeTypeName = "cube"; + + public override IPgTypeInfoResolver CreateResolver() => new Resolver(); + public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver(); + + public static void ThrowIfUnsupported(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + { + if (dataTypeName is { UnqualifiedNameSpan: "cube" or "_cube" } || type == typeof(NpgsqlCube)) + throw new NotSupportedException( + string.Format(NpgsqlStrings.CubeNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableCube), + typeof(TBuilder).Name)); + } + + class Resolver : IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new()); + + public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => Mappings.Find(type, dataTypeName, options); + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructType(CubeTypeName, + static (options, mapping, _) => mapping.CreateInfo(options, new CubeConverter()), isDefault: true); + + return mappings; + } + } + + sealed class ArrayResolver : Resolver, IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings)); + + public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => Mappings.Find(type, dataTypeName, options); + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructArrayType(CubeTypeName); + + return mappings; + } + } +} diff --git a/src/Npgsql/Internal/ResolverFactories/UnsupportedTypeInfoResolver.cs b/src/Npgsql/Internal/ResolverFactories/UnsupportedTypeInfoResolver.cs index 2d47f86807..efcc4633ba 100644 --- a/src/Npgsql/Internal/ResolverFactories/UnsupportedTypeInfoResolver.cs +++ b/src/Npgsql/Internal/ResolverFactories/UnsupportedTypeInfoResolver.cs @@ -16,6 +16,7 @@ sealed class UnsupportedTypeInfoResolver : IPgTypeInfoResolver RecordTypeInfoResolverFactory.ThrowIfUnsupported(type, dataTypeName, options); FullTextSearchTypeInfoResolverFactory.ThrowIfUnsupported(type, dataTypeName, options); LTreeTypeInfoResolverFactory.ThrowIfUnsupported(type, dataTypeName, options); + CubeTypeInfoResolverFactory.ThrowIfUnsupported(type, dataTypeName, options); JsonDynamicTypeInfoResolverFactory.Support.ThrowIfUnsupported(type, dataTypeName); diff --git a/src/Npgsql/NpgsqlDataSourceBuilder.cs b/src/Npgsql/NpgsqlDataSourceBuilder.cs index ce6926a544..68dd517ba0 100644 --- a/src/Npgsql/NpgsqlDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlDataSourceBuilder.cs @@ -59,7 +59,8 @@ internal static void ResetGlobalMappings(bool overwrite) new FullTextSearchTypeInfoResolverFactory(), new NetworkTypeInfoResolverFactory(), new GeometricTypeInfoResolverFactory(), - new LTreeTypeInfoResolverFactory() + new LTreeTypeInfoResolverFactory(), + new CubeTypeInfoResolverFactory() ], static () => { var builder = new PgTypeInfoResolverChainBuilder(); @@ -88,6 +89,7 @@ public NpgsqlDataSourceBuilder(string? connectionString = null) instance.AppendResolverFactory(new NetworkTypeInfoResolverFactory()); instance.AppendResolverFactory(new GeometricTypeInfoResolverFactory()); instance.AppendResolverFactory(new LTreeTypeInfoResolverFactory()); + instance.AppendResolverFactory(new CubeTypeInfoResolverFactory()); }; _internalBuilder.ConfigureResolverChain = static chain => chain.Add(UnsupportedTypeInfoResolver); _internalBuilder.EnableTransportSecurity(); diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 4a3d6fdad7..bc15fca563 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -588,6 +588,16 @@ public NpgsqlSlimDataSourceBuilder EnableLTree() return this; } + /// + /// Sets up mappings for the PostgreSQL cube extension type. + /// + /// The same builder instance so that multiple calls can be chained. + public NpgsqlSlimDataSourceBuilder EnableCube() + { + AddTypeInfoResolverFactory(new CubeTypeInfoResolverFactory()); + return this; + } + /// /// Sets up mappings for extra conversions from PostgreSQL to .NET types. /// diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs new file mode 100644 index 0000000000..15a89f56ee --- /dev/null +++ b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +// ReSharper disable once CheckNamespace +namespace NpgsqlTypes +{ + /// + /// Represents a PostgreSQl cube data type. + /// + /// + /// See https://www.postgresql.org/docs/current/cube.html + /// + public readonly struct NpgsqlCube : IEquatable + { + // Store the coordinates as a value tuple array + readonly double[] _lowerLeft; + readonly double[] _upperRight; + + /// + /// The lower left coordinates of the cube. + /// + public IReadOnlyList LowerLeft => _lowerLeft; + + /// + /// The upper right coordinates of the cube. + /// + public IReadOnlyList UpperRight => _upperRight; + + /// + /// The number of dimensions of the cube. + /// + public int Dimensions => _lowerLeft.Length; + + /// + /// True if the cube is a point, that is, the two defining corners are the same. + /// + public bool IsPoint { get; } + + /// + /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. + /// + /// This is an internal constructor to optimize the number of allocations. + /// The lower left values. + /// The upper right values. + /// + /// Thrown if the number of dimensions in the upper left and lower right values do not match. + /// + internal NpgsqlCube(double[] lowerLeft, double[] upperRight) + { + if (lowerLeft.Length != upperRight.Length) + throw new ArgumentException($"Not a valid cube: Different point dimensions in {lowerLeft} and {upperRight}."); + + IsPoint = lowerLeft.SequenceEqual(upperRight); + _lowerLeft = lowerLeft; + _upperRight = upperRight; + } + + /// + /// Makes a one dimensional cube with both coordinates the same. + /// + /// The point coordinate. + public NpgsqlCube(double coord) + { + IsPoint = true; + _lowerLeft = [coord]; + _upperRight = _lowerLeft; + } + + /// + /// Makes a one dimensional cube. + /// + /// The lower left value. + /// The upper right value. + public NpgsqlCube(double lowerLeft, double upperRight) + { + IsPoint = lowerLeft.CompareTo(upperRight) == 0; + _lowerLeft = [lowerLeft]; + _upperRight = IsPoint ? _lowerLeft : [upperRight]; + } + + /// + /// Makes a zero-volume cube using the coordinates defined by the array. + /// + /// The coordinates. + public NpgsqlCube(IEnumerable coords) + { + // Always create a defensive copy to prevent external mutation + _lowerLeft = coords.ToArray(); + IsPoint = true; + _upperRight = _lowerLeft; + } + + /// + /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. + /// + /// The lower left values. + /// The upper right values. + /// + /// Thrown if the number of dimensions in the upper left and lower right values do not match + /// or if the cube exceeds the maximum dimensions (100). + /// + public NpgsqlCube(IEnumerable lowerLeft, IEnumerable upperRight) : + this(lowerLeft.ToArray(), upperRight.ToArray()) + { } + + /// + /// Makes a new cube by adding a dimension on to an existing cube, with the same values for both endpoints of the new coordinate. + /// This is useful for building cubes piece by piece from calculated values. + /// + /// The existing cube. + /// The coordinate to add. + public NpgsqlCube(NpgsqlCube cube, double coord) + { + IsPoint = cube.IsPoint; + if (IsPoint) + { + _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); + _upperRight = _lowerLeft; + } + else + { + _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); + _upperRight = cube._upperRight.Append(coord).ToArray(); + } + } + + /// + /// Makes a new cube by adding a dimension on to an existing cube. + /// This is useful for building cubes piece by piece from calculated values. + /// + /// The existing cube. + /// The lower left value. + /// The upper right value. + public NpgsqlCube(NpgsqlCube cube, double lowerLeft, double upperRight) + { + IsPoint = cube.IsPoint && lowerLeft.CompareTo(upperRight) == 0; + if (IsPoint) + { + _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); + _upperRight = _lowerLeft; + } + else + { + _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); + _upperRight = cube._upperRight.Append(upperRight).ToArray(); + } + } + + /// + /// Makes a new cube from an existing cube, using a list of dimension indexes from an array. + /// Can be used to extract the endpoints of a single dimension, or to drop dimensions, or to reorder them as desired. + /// + /// The list of dimension indexes. + /// A new cube. + /// + /// + /// var cube = new NpgsqlCube(new[] { 1, 3, 5 }, new[] { 6, 7, 8 }); // '(1,3,5),(6,7,8)' + /// cube.ToSubset(1); // '(3),(7)' + /// cube.ToSubset(2, 1, 0, 0); // '(5,3,1,1),(8,7,6,6)' + /// + /// + public NpgsqlCube ToSubset(params int[] indexes) + { + var lowerLeft = new double[indexes.Length]; + var upperRight = new double[indexes.Length]; + + for (var i = 0; i < indexes.Length; i++) + { + lowerLeft[i] = _lowerLeft[indexes[i]]; + upperRight[i] = _upperRight[indexes[i]]; + } + + return new NpgsqlCube(lowerLeft, upperRight); + } + + /// + public bool Equals(NpgsqlCube other) => Dimensions == other.Dimensions + && _lowerLeft.SequenceEqual(other._lowerLeft) + && _upperRight.SequenceEqual(other._upperRight); + + /// + public override bool Equals(object? obj) => obj is NpgsqlCube other && Equals(other); + + /// + public static bool operator ==(NpgsqlCube x, NpgsqlCube y) => x.Equals(y); + + /// + public static bool operator !=(NpgsqlCube x, NpgsqlCube y) => !(x == y); + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + for (var i = 0; i < Dimensions; i++) + { + hashCode.Add(_lowerLeft[i]); + hashCode.Add(_upperRight[i]); + } + return hashCode.ToHashCode(); + } + + /// + /// Writes the cube in PostgreSQL's text format. + /// + void Write(StringBuilder stringBuilder) + { + var leftBuilder = new StringBuilder(); + var rightBuilder = new StringBuilder(); + + leftBuilder.Append('('); + rightBuilder.Append('('); + + for (var i = 0; i < Dimensions; i++) + { + leftBuilder.Append(_lowerLeft[i]); + rightBuilder.Append(_upperRight[i]); + + if (i >= Dimensions - 1) continue; + + leftBuilder.Append(", "); + rightBuilder.Append(", "); + } + + leftBuilder.Append(')'); + rightBuilder.Append(')'); + + if (IsPoint) + { + stringBuilder.Append(leftBuilder); + } + else + { + stringBuilder.Append(leftBuilder); + stringBuilder.Append(','); + stringBuilder.Append(rightBuilder); + } + } + + /// + /// Writes the cube in PostgreSQL's text format. + /// + public override string ToString() + { + var sb = new StringBuilder(); + Write(sb); + return sb.ToString(); + } + } +} diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs b/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs index 687ebf16b7..abb24c74d0 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs @@ -123,6 +123,12 @@ public enum NpgsqlDbType /// See https://www.postgresql.org/docs/current/static/datatype-geometric.html Polygon = 16, + /// + /// Corresponds to the PostgreSQL "cube" type, a geometric type representing multi-dimensional cubes. + /// + /// See https://www.postgresql.org/docs/current/cube.html + Cube = 63, // Extension type + #endregion #region Character Types @@ -740,6 +746,7 @@ public static DbType ToDbType(this NpgsqlDbType npgsqlDbType) // Plugin types NpgsqlDbType.Citext => "citext", + NpgsqlDbType.Cube => "cube", NpgsqlDbType.LQuery => "lquery", NpgsqlDbType.LTree => "ltree", NpgsqlDbType.LTxtQuery => "ltxtquery", @@ -964,6 +971,7 @@ _ when npgsqlDbType.HasFlag(NpgsqlDbType.Multirange) // Plugin types "citext" => NpgsqlDbType.Citext, + "cube" => NpgsqlDbType.Cube, "lquery" => NpgsqlDbType.LQuery, "ltree" => NpgsqlDbType.LTree, "ltxtquery" => NpgsqlDbType.LTxtQuery, diff --git a/src/Npgsql/Properties/NpgsqlStrings.Designer.cs b/src/Npgsql/Properties/NpgsqlStrings.Designer.cs index 7f71914ca2..d0b7839d6c 100644 --- a/src/Npgsql/Properties/NpgsqlStrings.Designer.cs +++ b/src/Npgsql/Properties/NpgsqlStrings.Designer.cs @@ -139,7 +139,16 @@ internal static string CannotUseValidationRootCertificateCallbackWithCustomValid return ResourceManager.GetString("CannotUseValidationRootCertificateCallbackWithCustomValidationCallback", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Cube isn't enabled; please call {0} on {1} to enable Cube.. + /// + internal static string CubeNotEnabled { + get { + return ResourceManager.GetString("CubeNotEnabled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type '{0}' required dynamic JSON serialization, which requires an explicit opt-in; call '{1}' on '{2}' or NpgsqlConnection.GlobalTypeMapper (see https://www.npgsql.org/doc/types/json.html and the 8.0 release notes for more details). Alternatively, if you meant to use Newtonsoft JSON.NET instead of System.Text.Json, call UseJsonNet() instead. ///. diff --git a/src/Npgsql/Properties/NpgsqlStrings.resx b/src/Npgsql/Properties/NpgsqlStrings.resx index af951d1a07..c39af4abc4 100644 --- a/src/Npgsql/Properties/NpgsqlStrings.resx +++ b/src/Npgsql/Properties/NpgsqlStrings.resx @@ -79,6 +79,9 @@ Ltree isn't enabled; please call {0} on {1} to enable LTree. + + Cube isn't enabled; please call {0} on {1} to enable Cube. + Ranges aren't enabled; please call {0} on {1} to enable ranges. diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index e7c8061376..7f28aa9e2e 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -27,6 +27,7 @@ Npgsql.NpgsqlMetricsOptions Npgsql.NpgsqlMetricsOptions.NpgsqlMetricsOptions() -> void Npgsql.NpgsqlSlimDataSourceBuilder.ConfigureTracing(System.Action! configureAction) -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.ConfigureTypeLoading(System.Action! configureAction) -> Npgsql.NpgsqlSlimDataSourceBuilder! +Npgsql.NpgsqlSlimDataSourceBuilder.EnableCube() -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.EnableGeometricTypes() -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.EnableJsonTypes() -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.EnableNetworkTypes() -> Npgsql.NpgsqlSlimDataSourceBuilder! @@ -93,6 +94,26 @@ NpgsqlTypes.NpgsqlBox.Deconstruct(out double left, out double right, out double NpgsqlTypes.NpgsqlBox.Deconstruct(out NpgsqlTypes.NpgsqlPoint lowerLeft, out NpgsqlTypes.NpgsqlPoint upperRight) -> void NpgsqlTypes.NpgsqlCircle.Deconstruct(out double x, out double y, out double radius) -> void NpgsqlTypes.NpgsqlCircle.Deconstruct(out NpgsqlTypes.NpgsqlPoint center, out double radius) -> void +NpgsqlTypes.NpgsqlCube +NpgsqlTypes.NpgsqlCube.NpgsqlCube() -> void +NpgsqlTypes.NpgsqlCube.Dimensions.get -> int +NpgsqlTypes.NpgsqlCube.Equals(NpgsqlTypes.NpgsqlCube other) -> bool +NpgsqlTypes.NpgsqlCube.LowerLeft.get -> System.Collections.Generic.IReadOnlyList! +NpgsqlTypes.NpgsqlCube.NpgsqlCube(double coord) -> void +NpgsqlTypes.NpgsqlCube.NpgsqlCube(double lowerLeft, double upperRight) -> void +NpgsqlTypes.NpgsqlCube.NpgsqlCube(NpgsqlTypes.NpgsqlCube cube, double coord) -> void +NpgsqlTypes.NpgsqlCube.NpgsqlCube(NpgsqlTypes.NpgsqlCube cube, double lowerLeft, double upperRight) -> void +NpgsqlTypes.NpgsqlCube.NpgsqlCube(System.Collections.Generic.IEnumerable! coords) -> void +NpgsqlTypes.NpgsqlCube.NpgsqlCube(System.Collections.Generic.IEnumerable! lowerLeft, System.Collections.Generic.IEnumerable! upperRight) -> void +NpgsqlTypes.NpgsqlCube.IsPoint.get -> bool +NpgsqlTypes.NpgsqlCube.ToSubset(params int[]! indexes) -> NpgsqlTypes.NpgsqlCube +NpgsqlTypes.NpgsqlCube.UpperRight.get -> System.Collections.Generic.IReadOnlyList! +NpgsqlTypes.NpgsqlDbType.Cube = 63 -> NpgsqlTypes.NpgsqlDbType +override NpgsqlTypes.NpgsqlCube.Equals(object? obj) -> bool +override NpgsqlTypes.NpgsqlCube.GetHashCode() -> int +override NpgsqlTypes.NpgsqlCube.ToString() -> string! +static NpgsqlTypes.NpgsqlCube.operator !=(NpgsqlTypes.NpgsqlCube x, NpgsqlTypes.NpgsqlCube y) -> bool +static NpgsqlTypes.NpgsqlCube.operator ==(NpgsqlTypes.NpgsqlCube x, NpgsqlTypes.NpgsqlCube y) -> bool NpgsqlTypes.NpgsqlLine.Deconstruct(out double a, out double b, out double c) -> void NpgsqlTypes.NpgsqlLSeg.Deconstruct(out NpgsqlTypes.NpgsqlPoint start, out NpgsqlTypes.NpgsqlPoint end) -> void NpgsqlTypes.NpgsqlPoint.Deconstruct(out double x, out double y) -> void diff --git a/test/Npgsql.Tests/Types/CubeTests.cs b/test/Npgsql.Tests/Types/CubeTests.cs new file mode 100644 index 0000000000..8b766c0366 --- /dev/null +++ b/test/Npgsql.Tests/Types/CubeTests.cs @@ -0,0 +1,277 @@ +using System; +using System.Threading.Tasks; +using Npgsql.Properties; +using NpgsqlTypes; +using NUnit.Framework; + +namespace Npgsql.Tests.Types; + +public class CubeTests : MultiplexingTestBase +{ + static readonly TestCaseData[] CubeValues = + { + new TestCaseData(new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }, new[] { 4.0, 5.0, 6.0 }), "(1, 2, 3),(4, 5, 6)") + .SetName("Cube_MultiDimensional"), + new TestCaseData(new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }), "(1, 2, 3)") + .SetName("Cube_MultiDimensionalPoint"), + new TestCaseData(new NpgsqlCube(1.0), "(1)") + .SetName("Cube_SingleDimensionalPoint"), + new TestCaseData(new NpgsqlCube(1.0, 2.0), "(1),(2)") + .SetName("Cube_SingleDimensional") + }; + + [Test, TestCaseSource(nameof(CubeValues))] + public Task Cube(NpgsqlCube cube, string sqlLiteral) + => AssertType(cube, sqlLiteral, "cube", NpgsqlDbType.Cube, isDefault: true, isNpgsqlDbTypeInferredFromClrType: false); + + [Test] + public void Cube_Constructor_SingleValue() + { + var cube = new NpgsqlCube(1.0); + Assert.That(cube.IsPoint, Is.True); + Assert.That(cube.Dimensions, Is.EqualTo(1)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 1.0 })); + } + + [Test] + public void Cube_Constructor_SingleCoord_Point() + { + var cube = new NpgsqlCube(1.0, 1.0); + Assert.That(cube.IsPoint, Is.True); + Assert.That(cube.Dimensions, Is.EqualTo(1)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 1.0 })); + } + + [Test] + public void Cube_Constructor_SingleCoord_NotPoint() + { + var cube = new NpgsqlCube(1.0, 2.0); + Assert.That(cube.IsPoint, Is.False); + Assert.That(cube.Dimensions, Is.EqualTo(1)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 2.0 })); + } + + [Test] + public void Cube_Constructor_LowerLeft_UpperRight_NotPoint() + { + var cube = new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }); + Assert.That(cube.IsPoint, Is.False); + Assert.That(cube.Dimensions, Is.EqualTo(2)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 3.0, 4.0 })); + } + + [Test] + public void Cube_Constructor_LowerLeft_UpperRight_Point() + { + var cube = new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 1.0, 2.0 }); + Assert.That(cube.IsPoint, Is.True); + Assert.That(cube.Dimensions, Is.EqualTo(2)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 1.0, 2.0 })); + } + + [Test] + public void Cube_Constructor_AddDimension_Single_Point() + { + var existingCube = new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }); + var cube = new NpgsqlCube(existingCube, 4.0); + Assert.That(cube.IsPoint, Is.True); + Assert.That(cube.Dimensions, Is.EqualTo(4)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0, 3.0, 4.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 1.0, 2.0, 3.0, 4.0 })); + } + + [Test] + public void Cube_Constructor_AddDimension_Single_NotPoint() + { + var existingCube = new NpgsqlCube(new [] { 1.0, 2.0 }, new [] { 3.0, 4.0 }); + var cube = new NpgsqlCube(existingCube, 3.0); + Assert.That(cube.IsPoint, Is.False); + Assert.That(cube.Dimensions, Is.EqualTo(3)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0, 3.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 3.0, 4.0, 3.0 })); + } + + [Test] + public void Cube_Constructor_AddDimension_LowerLeft_UpperRight_Point() + { + var existingCube = new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }); + var cube = new NpgsqlCube(existingCube, 4.0, 4.0); + Assert.That(cube.IsPoint, Is.True); + Assert.That(cube.Dimensions, Is.EqualTo(4)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0, 3.0, 4.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 1.0, 2.0, 3.0, 4.0 })); + } + + [Test] + public void Cube_Constructor_AddDimension_LowerLeft_UpperRight_NotPoint() + { + var existingCube = new NpgsqlCube(new [] { 1.0, 2.0 }, new [] { 3.0, 4.0 }); + var cube = new NpgsqlCube(existingCube, 4.0, 5.0); + Assert.That(cube.IsPoint, Is.False); + Assert.That(cube.Dimensions, Is.EqualTo(3)); + Assert.That(cube.LowerLeft, Is.EquivalentTo(new [] { 1.0, 2.0, 4.0 })); + Assert.That(cube.UpperRight, Is.EquivalentTo(new [] { 3.0, 4.0, 5.0 })); + } + + [Test] + public void Cube_Subset() + { + var cube = new NpgsqlCube(new [] { 1.0, 2.0, 3.0 }, new [] { 4.0, 5.0, 6.0 }); + Assert.That(cube.ToSubset(0, 2, 1, 1), Is.EqualTo(new NpgsqlCube(new [] { 1.0, 3.0, 2.0, 2.0 }, new [] { 4.0, 6.0, 5.0, 5.0 }))); + } + + [Test] + public void Cube_ToString_NotPoint() + { + var cube = new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }, new[] { 4.0, 5.0, 6.0 }); + Assert.That(cube.ToString(), Is.EqualTo("(1, 2, 3),(4, 5, 6)")); + } + + [Test] + public void Cube_ToString_Point() + { + var cube = new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }); + Assert.That(cube.ToString(), Is.EqualTo("(1, 2, 3)")); + } + + [Test] + public async Task Cube_Array() + { + var data = new[] + { + new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }), + new NpgsqlCube(new[] { 5.0, 6.0 }), + new NpgsqlCube(1.0, 2.0) + }; + + await AssertType( + data, + @"{""(1, 2),(3, 4)"",""(5, 6)"",""(1),(2)""}", + "cube[]", + NpgsqlDbType.Cube | NpgsqlDbType.Array, + isDefault: true, + isNpgsqlDbTypeInferredFromClrType: false); + } + + [Test] + public void Cube_DimensionMismatch_ThrowsArgumentException() + { + var ex = Assert.Throws(() => new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0 })); + Assert.That(ex!.Message, Does.Contain("Different point dimensions")); + } + + [Test] + public Task Cube_NegativeValues() + => AssertType( + new NpgsqlCube(new[] { -1.0, -2.0, -3.0 }, new[] { -4.0, -5.0, -6.0 }), + "(-1, -2, -3),(-4, -5, -6)", + "cube", + NpgsqlDbType.Cube, + isDefault: true, + isNpgsqlDbTypeInferredFromClrType: false); + + [Test] + public void Cube_Equality_HashCode() + { + var cube1 = new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }); + var cube2 = new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 }); + var cube3 = new NpgsqlCube(new[] { 1.0, 2.0 }, new[] { 3.0, 5.0 }); + + // Test equality + Assert.That(cube1, Is.EqualTo(cube2)); + Assert.That(cube1 == cube2, Is.True); + Assert.That(cube1 != cube3, Is.True); + Assert.That(cube1.Equals(cube2), Is.True); + Assert.That(cube1.Equals(cube3), Is.False); + + // Test hash code consistency + Assert.That(cube1.GetHashCode(), Is.EqualTo(cube2.GetHashCode())); + Assert.That(cube1.GetHashCode(), Is.Not.EqualTo(cube3.GetHashCode())); + } + + [Test] + public Task Cube_ZeroValues() + => AssertType( + new NpgsqlCube(0.0, 0.0), + "(0)", + "cube", + NpgsqlDbType.Cube, + isDefault: true, + isNpgsqlDbTypeInferredFromClrType: false); + + [Test] + public Task Cube_MaxDimensions() + { + var lowerLeft = new double[100]; + var upperRight = new double[100]; + for (var i = 0; i < 100; i++) + { + lowerLeft[i] = i; + upperRight[i] = i + 100; + } + + var expectedLower = string.Join(", ", lowerLeft); + var expectedUpper = string.Join(", ", upperRight); + var expected = $"({expectedLower}),({expectedUpper})"; + + return AssertType( + new NpgsqlCube(lowerLeft, upperRight), + expected, + "cube", + NpgsqlDbType.Cube, + isDefault: true, + isNpgsqlDbTypeInferredFromClrType: false); + } + + [Test] + public async Task Cube_not_supported_by_default_on_NpgsqlSlimSourceBuilder() + { + var errorMessage = string.Format( + NpgsqlStrings.CubeNotEnabled, nameof(NpgsqlSlimDataSourceBuilder.EnableCube), nameof(NpgsqlSlimDataSourceBuilder)); + + var dataSourceBuilder = new NpgsqlSlimDataSourceBuilder(ConnectionString); + await using var dataSource = dataSourceBuilder.Build(); + + var exception = + await AssertTypeUnsupportedRead("(1),(2)", "cube", dataSource); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); + exception = await AssertTypeUnsupportedWrite(new NpgsqlCube(1.0, 2.0), "cube", dataSource); + Assert.That(exception.InnerException!.Message, Is.EqualTo(errorMessage)); + } + + [Test] + public async Task NpgsqlSlimSourceBuilder_EnableCube() + { + var dataSourceBuilder = new NpgsqlSlimDataSourceBuilder(ConnectionString); + dataSourceBuilder.EnableCube(); + await using var dataSource = dataSourceBuilder.Build(); + + await AssertType(dataSource, new NpgsqlCube(1.0, 2.0), "(1),(2)", "cube", NpgsqlDbType.Cube, isDefaultForWriting: false, skipArrayCheck: true); + } + + [Test] + public async Task NpgsqlSlimSourceBuilder_EnableArrays() + { + var dataSourceBuilder = new NpgsqlSlimDataSourceBuilder(ConnectionString); + dataSourceBuilder.EnableCube(); + dataSourceBuilder.EnableArrays(); + await using var dataSource = dataSourceBuilder.Build(); + + await AssertType(dataSource, new NpgsqlCube(1.0, 2.0), "(1),(2)", "cube", NpgsqlDbType.Cube, isDefaultForWriting: false); + } + + [OneTimeSetUp] + public async Task SetUp() + { + await using var conn = await OpenConnectionAsync(); + TestUtil.MinimumPgVersion(conn, "13.0"); + await TestUtil.EnsureExtensionAsync(conn, "cube"); + } + + public CubeTests(MultiplexingMode multiplexingMode) : base(multiplexingMode) { } +} From fcf2d7fe6370243759637cdcd14e241b1c2e5ad7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:17:26 +0100 Subject: [PATCH 113/155] Bump NUnit.Analyzers from 4.11.1 to 4.11.2 (#6288) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6a99e09cfc..e70fd53bb5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + From ed512e5297894f9cd9751b540ac6de9f1f493388 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 114/155] handles SSL ConnectionReset on Windows; fixes #6274 (#6287) Fixes #6274 --- src/Npgsql/Internal/NpgsqlConnector.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 2ceed5cfb9..6749773781 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -609,8 +609,7 @@ static async Task OpenCore( // Any error after trying with GSS encryption (gssEncMode == GssEncryptionMode.Prefer || // Auth error with/without SSL - (e is PostgresException { SqlState: PostgresErrorCodes.InvalidAuthorizationSpecification } && - (sslMode == SslMode.Prefer && conn.IsSslEncrypted || sslMode == SslMode.Allow && !conn.IsSslEncrypted))) + (sslMode == SslMode.Prefer && conn.IsSslEncrypted || sslMode == SslMode.Allow && !conn.IsSslEncrypted)) { if (gssEncMode == GssEncryptionMode.Prefer) { From 8fd4968565b8f3c0bdce914e0cd6ca81ab938090 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Wed, 5 Nov 2025 17:26:57 +0100 Subject: [PATCH 115/155] Remove task cancellation helpers (#6291) --- src/Npgsql/Internal/NpgsqlConnector.cs | 40 +++-- src/Npgsql/TaskTimeoutAndCancellation.cs | 66 ------- .../TaskTimeoutAndCancellationTest.cs | 162 ------------------ 3 files changed, 24 insertions(+), 244 deletions(-) delete mode 100644 src/Npgsql/TaskTimeoutAndCancellation.cs delete mode 100644 test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 6749773781..098277dd47 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1315,9 +1315,6 @@ void Connect(NpgsqlTimeout timeout) async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken) { - // Whether the framework and/or the OS platform support Dns.GetHostAddressesAsync cancellation API or they do not, - // we always fake-cancel the operation with the help of TaskTimeoutAndCancellation.ExecuteAsync. It stops waiting - // and raises the exception, while the actual task may be left running. EndPoint[] endpoints; if (NpgsqlConnectionStringBuilder.IsUnixSocket(Host, Port, out var socketPath)) { @@ -1328,8 +1325,18 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok IPAddress[] ipAddresses; try { - ipAddresses = await Dns.GetHostAddressesAsync(Host, cancellationToken) - .WaitAsync(timeout.CheckAndGetTimeLeft(), cancellationToken).ConfigureAwait(false); + 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(); + } } catch (SocketException ex) { @@ -1361,7 +1368,18 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok // Some options are not applied after the socket is open, see #6013 SetSocketOptions(socket); - await OpenSocketConnectionAsync(socket, endpoint, endpointTimeout, cancellationToken).ConfigureAwait(false); + using var combinedCts = endpointTimeout.IsSet ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; + combinedCts?.CancelAfter(endpointTimeout.CheckAndGetTimeLeft()); + var combinedToken = combinedCts?.Token ?? cancellationToken; + try + { + await socket.ConnectAsync(endpoint, combinedToken).ConfigureAwait(false); + } + catch (OperationCanceledException oce) when ( + oce.CancellationToken == combinedToken && !cancellationToken.IsCancellationRequested) + { + throw new TimeoutException(); + } _socket = socket; ConnectedEndPoint = endpoint; @@ -1389,16 +1407,6 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok throw new NpgsqlException($"Failed to connect to {endpoint}", e); } } - - static Task OpenSocketConnectionAsync(Socket socket, EndPoint endpoint, NpgsqlTimeout perIpTimeout, CancellationToken cancellationToken) - { - // Whether the OS platform supports Socket.ConnectAsync cancellation API or not, - // we always fake-cancel the operation with the help of TaskTimeoutAndCancellation.ExecuteAsync. It stops waiting - // and raises the exception, while the actual task may be left running. - Task ConnectAsync(CancellationToken ct) => - socket.ConnectAsync(endpoint, ct).AsTask(); - return TaskTimeoutAndCancellation.ExecuteAsync(ConnectAsync, perIpTimeout, cancellationToken); - } } EndPoint[] IPAddressesToEndpoints(IPAddress[] ipAddresses, int port) diff --git a/src/Npgsql/TaskTimeoutAndCancellation.cs b/src/Npgsql/TaskTimeoutAndCancellation.cs deleted file mode 100644 index ceed87ba94..0000000000 --- a/src/Npgsql/TaskTimeoutAndCancellation.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Npgsql.Util; - -namespace Npgsql; - -/// -/// Utility class to execute a potentially non-cancellable while allowing to timeout and/or cancel awaiting for it and at the same time prevent event if the original fails later. -/// -static class TaskTimeoutAndCancellation -{ - /// - /// Executes a potentially non-cancellable while allowing to timeout and/or cancel awaiting for it. - /// If the given task does not complete within , a is thrown. - /// The executed may be left in an incomplete state after the that this method returns completes dues to timeout and/or cancellation request. - /// The method guarantees that the abandoned, incomplete is not going to produce event if it fails later. - /// - /// Gets the for execution with a combined that attempts to cancel the in an event of the timeout or external cancellation request. - /// The timeout after which the should be faulted with a if it hasn't otherwise completed. - /// The to monitor for a cancellation request. - /// The result . - /// The representing the asynchronous wait. - internal static async Task ExecuteAsync(Func> getTaskFunc, NpgsqlTimeout timeout, CancellationToken cancellationToken) - { - Task? task = default; - await ExecuteAsync(ct => (Task)(task = getTaskFunc(ct)), timeout, cancellationToken).ConfigureAwait(false); - return await task!.ConfigureAwait(false); - } - - /// - /// Executes a potentially non-cancellable while allowing to timeout and/or cancel awaiting for it. - /// If the given task does not complete within , a is thrown. - /// The executed may be left in an incomplete state after the that this method returns completes dues to timeout and/or cancellation request. - /// The method guarantees that the abandoned, incomplete is not going to produce event if it fails later. - /// - /// Gets the for execution with a combined that attempts to cancel the in an event of the timeout or external cancellation request. - /// The timeout after which the should be faulted with a if it hasn't otherwise completed. - /// The to monitor for a cancellation request. - /// The representing the asynchronous wait. - internal static async Task ExecuteAsync(Func getTaskFunc, NpgsqlTimeout timeout, CancellationToken cancellationToken) - { - using var combinedCts = timeout.IsSet ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; - var task = getTaskFunc(combinedCts?.Token ?? cancellationToken); - try - { - try - { - await task.WaitAsync(timeout.CheckAndGetTimeLeft(), cancellationToken).ConfigureAwait(false); - } - catch (TimeoutException) when (!task!.IsCompleted) - { - // Attempt to stop the Task in progress. - combinedCts?.Cancel(); - throw; - } - } - catch - { - // Prevent unobserved Task notifications by observing the failed Task exception. - // To test: comment the next line out and re-run TaskExtensionsTest.DelayedFaultedTaskCancellation. - _ = task.ContinueWith(t => _ = t.Exception, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Current); - throw; - } - } -} diff --git a/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs b/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs deleted file mode 100644 index f90bd1dd92..0000000000 --- a/test/Npgsql.Tests/TaskTimeoutAndCancellationTest.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using Npgsql.Util; - -namespace Npgsql.Tests; - -[NonParallelizable] // To make sure unobserved tasks from other tests do not leak -public class TaskTimeoutAndCancellationTest : TestBase -{ - const int TestResultValue = 777; - - async Task GetResultTaskAsync(int timeout, CancellationToken ct) - { - await Task.Delay(timeout, ct); - return TestResultValue; - } - - Task GetVoidTaskAsync(int timeout, CancellationToken ct) => Task.Delay(timeout, ct); - - [Test] - public async Task SuccessfulResultTaskAsync() => - Assert.That(await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetResultTaskAsync(10, ct), NpgsqlTimeout.Infinite, CancellationToken.None), Is.EqualTo(TestResultValue)); - - [Test] - public async Task SuccessfulVoidTaskAsync() => - await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetVoidTaskAsync(10, ct), NpgsqlTimeout.Infinite, CancellationToken.None); - - [Test] - public void InfinitelyLongTaskTimeout() => - Assert.ThrowsAsync(async () => - await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetVoidTaskAsync(Timeout.Infinite, ct), new NpgsqlTimeout(TimeSpan.FromMilliseconds(10)), CancellationToken.None)); - - [Test] - public void InfinitelyLongTaskCancellation() - { - using var cts = new CancellationTokenSource(10); - Assert.ThrowsAsync(async () => - await TaskTimeoutAndCancellation.ExecuteAsync(ct => GetVoidTaskAsync(Timeout.Infinite, ct), NpgsqlTimeout.Infinite, cts.Token)); - } - - /// - /// The test creates a delayed execution Task that is being fake-cancelled and fails subsequently and triggers 'TaskScheduler.UnobservedTaskException event'. - /// - /// - /// The test is based on timing and depends on availability of thread pool threads. Therefore it could become unstable if the environment is under pressure. - /// - [Theory, IssueLink("https://github.com/npgsql/npgsql/issues/4149")] - [TestCase("CancelAndTimeout")] - [TestCase("CancelOnly")] - [TestCase("TimeoutOnly")] - [TestCase("CancelAndTimeout")] - [TestCase("CancelOnly")] - [TestCase("TimeoutOnly")] - public Task DelayedFaultedTaskCancellation(string testCase) => RunDelayedFaultedTaskTestAsync(async getUnobservedTaskException => - { - var cancel = true; - var timeout = true; - switch (testCase) - { - case "TimeoutOnly": - cancel = false; - break; - case "CancelOnly": - timeout = false; - break; - } - - var notifyDelayCompleted = new SemaphoreSlim(0, 1); - - // Invoke the method that creates a delayed execution Task that fails subsequently. - await CreateTaskAndPreemptWithCancellationAsync(500, cancel, timeout, notifyDelayCompleted); - - // Wait enough time for the non-cancelable task to notify us that an exception is thrown. - await notifyDelayCompleted.WaitAsync(); - - // And then wait some more. - var repeatCount = 2; - while (getUnobservedTaskException() is null && repeatCount-- > 0) - { - await Task.Delay(100); - - // Run the garbage collector to collect unobserved Tasks. - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - }); - - static async Task RunDelayedFaultedTaskTestAsync(Func, Task> test) - { - // Run the garbage collector to collect unobserved Tasks from other tests. - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - Exception? unobservedTaskException = null; - - // Subscribe to UnobservedTaskException event to store the Exception, if any. - void OnUnobservedTaskException(object? source, UnobservedTaskExceptionEventArgs args) - { - if (!args.Observed) - { - args.SetObserved(); - } - unobservedTaskException = args.Exception; - } - TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; - - try - { - await test(() => unobservedTaskException); - - // Verify the unobserved Task exception event has not been received. - Assert.That(unobservedTaskException, Is.Null, unobservedTaskException?.Message); - } - finally - { - TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; - } - } - - /// - /// Create a delayed execution, non-Cancellable Task that fails subsequently after the Task goes out of scope. - /// - static async Task CreateTaskAndPreemptWithCancellationAsync(int delayMs, bool cancel, bool timeout, SemaphoreSlim notifyDelayCompleted) - { - var nonCancellableTask = Task.Delay(delayMs, CancellationToken.None) - .ContinueWith( - async _ => - { - try - { - await Task.FromException(new Exception("Unobserved Task Test Exception")); - } - finally - { - notifyDelayCompleted.Release(); - } - }) - .Unwrap(); - - var timeoutMs = delayMs / 5; - using var cts = cancel ? new CancellationTokenSource(timeoutMs) : null; - try - { - await TaskTimeoutAndCancellation.ExecuteAsync( - _ => nonCancellableTask, - timeout ? new NpgsqlTimeout(TimeSpan.FromMilliseconds(timeoutMs)) : NpgsqlTimeout.Infinite, - cts?.Token ?? CancellationToken.None); - } - catch (TimeoutException) - { - // Expected due to preemptive time out. - } - catch (OperationCanceledException) when (cts?.IsCancellationRequested == true) - { - // Expected due to preemptive cancellation. - } - Assert.That(nonCancellableTask.IsCompleted, Is.False); - } -} From 88cfc120f79a38ed7ba9ada60481f4c0819d7cd4 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 5 Nov 2025 20:04:30 +0300 Subject: [PATCH 116/155] =?UTF-8?q?NpgsqlMultiHostDataSource:=20rethrow=20?= =?UTF-8?q?OperationCanceledException=20on=20canc=E2=80=A6=20(#6283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6282 --- src/Npgsql/NpgsqlMultiHostDataSource.cs | 6 ++++++ test/Npgsql.Tests/MultipleHostsTests.cs | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Npgsql/NpgsqlMultiHostDataSource.cs b/src/Npgsql/NpgsqlMultiHostDataSource.cs index 4997a6093a..5d21ab8954 100644 --- a/src/Npgsql/NpgsqlMultiHostDataSource.cs +++ b/src/Npgsql/NpgsqlMultiHostDataSource.cs @@ -219,6 +219,12 @@ static bool IsOnline(DatabaseState state, TargetSessionAttributes preferredType) } } } + catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && oce.CancellationToken == cancellationToken) + { + if (connector is not null) + pool.Return(connector); + throw; + } catch (Exception ex) { exceptions.Add(ex); diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index 66c1a99cf7..9025586c55 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -1133,6 +1133,17 @@ public async Task Build_with_multiple_hosts_is_supported() await using var connection = await dataSource.OpenConnectionAsync(); } + [Test] + public async Task OpenConnection_when_canceled_throws_TaskCanceledException() + { + var builder = new NpgsqlDataSourceBuilder(ConnectionString); + await using var dataSource = builder.BuildMultiHost(); + Assert.ThrowsAsync(async () => + { + await using var connection = await dataSource.OpenConnectionAsync(new CancellationToken(true)); + }); + } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4181")] [Explicit("Fails until #4181 is fixed.")] public async Task LoadBalancing_is_fair_if_first_host_is_down([Values]TargetSessionAttributes targetSessionAttributes) From a68839b9c8c4aaec343e05ca02f89175f3cf99a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:51:37 +0100 Subject: [PATCH 117/155] Bump BenchmarkDotNet from 0.15.5 to 0.15.6 (#6293) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e70fd53bb5..758397b7cd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + From c3cf9f94b09cfe6252e1e3122ff4764247f120e2 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 6 Nov 2025 11:09:06 +0300 Subject: [PATCH 118/155] Allow specifying multiple root certificates in NpgsqlDataSourceBuilder (#6057) Closes #6056 --- src/Npgsql/Internal/NpgsqlConnector.cs | 16 ++++---- .../Internal/TransportSecurityHandler.cs | 4 +- src/Npgsql/NpgsqlDataSourceBuilder.cs | 30 +++++++++++++- src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 36 ++++++++++++++-- src/Npgsql/PublicAPI.Unshipped.txt | 4 ++ test/Npgsql.Tests/SecurityTests.cs | 41 +++++++++++++++++++ 6 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 098277dd47..40dcf6e941 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1127,7 +1127,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, var checkCertificateRevocation = Settings.CheckCertificateRevocation; RemoteCertificateValidationCallback? certificateValidationCallback; - X509Certificate2? caCert; + X509Certificate2Collection? caCerts; string? certRootPath = null; if (sslMode is SslMode.Prefer or SslMode.Require) @@ -1135,11 +1135,11 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, certificateValidationCallback = SslTrustServerValidation; checkCertificateRevocation = false; } - else if ((caCert = DataSource.TransportSecurityHandler.RootCertificateCallback?.Invoke()) is not null || + else if (((caCerts = DataSource.TransportSecurityHandler.RootCertificatesCallback?.Invoke()) is not null && caCerts.Count > 0) || (certRootPath = Settings.RootCertificate ?? PostgresEnvironment.SslCertRoot ?? PostgresEnvironment.SslCertRootDefault) is not null) { - certificateValidationCallback = SslRootValidation(sslMode == SslMode.VerifyFull, certRootPath, caCert); + certificateValidationCallback = SslRootValidation(sslMode == SslMode.VerifyFull, certRootPath, caCerts); } else if (sslMode == SslMode.VerifyCA) { @@ -1195,7 +1195,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, if (Settings.RootCertificate is not null) throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithCustomValidationCallback); - if (DataSource.TransportSecurityHandler.RootCertificateCallback is not null) + if (DataSource.TransportSecurityHandler.RootCertificatesCallback is not null) throw new ArgumentException(NpgsqlStrings.CannotUseValidationRootCertificateCallbackWithCustomValidationCallback); } } @@ -1984,7 +1984,7 @@ internal void ClearTransaction(Exception? disposeReason = null) (sender, certificate, chain, sslPolicyErrors) => true; - static RemoteCertificateValidationCallback SslRootValidation(bool verifyFull, string? certRootPath, X509Certificate2? caCertificate) + static RemoteCertificateValidationCallback SslRootValidation(bool verifyFull, string? certRootPath, X509Certificate2Collection? caCertificates) => (_, certificate, chain, sslPolicyErrors) => { if (certificate is null || chain is null) @@ -2001,12 +2001,12 @@ static RemoteCertificateValidationCallback SslRootValidation(bool verifyFull, st if (certRootPath is null) { - Debug.Assert(caCertificate is not null); - certs.Add(caCertificate); + Debug.Assert(caCertificates is { Count: > 0 }); + certs.AddRange(caCertificates); } else { - Debug.Assert(caCertificate is null); + Debug.Assert(caCertificates is null or { Count: > 0 }); if (Path.GetExtension(certRootPath).ToUpperInvariant() != ".PFX") certs.ImportFromPemFile(certRootPath); diff --git a/src/Npgsql/Internal/TransportSecurityHandler.cs b/src/Npgsql/Internal/TransportSecurityHandler.cs index 9945e80534..fbe8cad72e 100644 --- a/src/Npgsql/Internal/TransportSecurityHandler.cs +++ b/src/Npgsql/Internal/TransportSecurityHandler.cs @@ -11,7 +11,7 @@ class TransportSecurityHandler { public virtual bool SupportEncryption => false; - public virtual Func? RootCertificateCallback + public virtual Func? RootCertificatesCallback { get => throw new NotSupportedException(string.Format(NpgsqlStrings.TransportSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableTransportSecurity))); set => throw new NotSupportedException(string.Format(NpgsqlStrings.TransportSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableTransportSecurity))); @@ -29,7 +29,7 @@ sealed class RealTransportSecurityHandler : TransportSecurityHandler { public override bool SupportEncryption => true; - public override Func? RootCertificateCallback { get; set; } + public override Func? RootCertificatesCallback { get; set; } public override Task NegotiateEncryption(bool async, NpgsqlConnector connector, SslMode sslMode, NpgsqlTimeout timeout, CancellationToken cancellationToken) => connector.NegotiateEncryption(sslMode, timeout, async, cancellationToken); diff --git a/src/Npgsql/NpgsqlDataSourceBuilder.cs b/src/Npgsql/NpgsqlDataSourceBuilder.cs index 68dd517ba0..b08f175533 100644 --- a/src/Npgsql/NpgsqlDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlDataSourceBuilder.cs @@ -41,7 +41,7 @@ public INpgsqlNameTranslator DefaultNameTranslator } /// - /// A connection string builder that can be used to configured the connection string on the builder. + /// A connection string builder that can be used to configure the connection string on the builder. /// public NpgsqlConnectionStringBuilder ConnectionStringBuilder => _internalBuilder.ConnectionStringBuilder; @@ -297,6 +297,17 @@ public NpgsqlDataSourceBuilder UseRootCertificate(X509Certificate2? rootCertific return this; } + /// + /// Sets the that will be used validate SSL certificate, received from the server. + /// + /// The CA certificates. + /// The same builder instance so that multiple calls can be chained. + public NpgsqlDataSourceBuilder UseRootCertificates(X509Certificate2Collection? rootCertificates) + { + _internalBuilder.UseRootCertificates(rootCertificates); + return this; + } + /// /// Specifies a callback that will be used to validate SSL certificate, received from the server. /// @@ -313,6 +324,23 @@ public NpgsqlDataSourceBuilder UseRootCertificateCallback(Func return this; } + /// + /// Specifies a callback that will be used to validate SSL certificate, received from the server. + /// + /// The callback to get CA certificates. + /// The same builder instance so that multiple calls can be chained. + /// + /// This overload, which accepts a callback, is suitable for scenarios where the certificate rotates + /// and might change during the lifetime of the application. + /// When that's not the case, use the overload which directly accepts the certificate. + /// + /// The same builder instance so that multiple calls can be chained. + public NpgsqlDataSourceBuilder UseRootCertificatesCallback(Func? rootCertificateCallback) + { + _internalBuilder.UseRootCertificatesCallback(rootCertificateCallback); + return this; + } + /// /// Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the /// recommended way to fetch a rotating access token. diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index bc15fca563..8bfe449c4a 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -60,7 +60,7 @@ public sealed class NpgsqlSlimDataSourceBuilder : INpgsqlTypeMapper internal Action ConfigureDefaultFactories { get; set; } /// - /// A connection string builder that can be used to configured the connection string on the builder. + /// A connection string builder that can be used to configure the connection string on the builder. /// public NpgsqlConnectionStringBuilder ConnectionStringBuilder { get; } @@ -252,9 +252,19 @@ public NpgsqlSlimDataSourceBuilder UseClientCertificatesCallback(ActionThe same builder instance so that multiple calls can be chained. public NpgsqlSlimDataSourceBuilder UseRootCertificate(X509Certificate2? rootCertificate) => rootCertificate is null - ? UseRootCertificateCallback(null) + ? UseRootCertificatesCallback((Func?)null) : UseRootCertificateCallback(() => rootCertificate); + /// + /// Sets the that will be used validate SSL certificate, received from the server. + /// + /// The CA certificates. + /// The same builder instance so that multiple calls can be chained. + public NpgsqlSlimDataSourceBuilder UseRootCertificates(X509Certificate2Collection? rootCertificates) + => rootCertificates is null + ? UseRootCertificatesCallback((Func?)null) + : UseRootCertificatesCallback(() => rootCertificates); + /// /// Specifies a callback that will be used to validate SSL certificate, received from the server. /// @@ -268,7 +278,27 @@ public NpgsqlSlimDataSourceBuilder UseRootCertificate(X509Certificate2? rootCert /// The same builder instance so that multiple calls can be chained. public NpgsqlSlimDataSourceBuilder UseRootCertificateCallback(Func? rootCertificateCallback) { - _transportSecurityHandler.RootCertificateCallback = rootCertificateCallback; + _transportSecurityHandler.RootCertificatesCallback = () => rootCertificateCallback is not null + ? new X509Certificate2Collection(rootCertificateCallback()) + : null; + + return this; + } + + /// + /// Specifies a callback that will be used to validate SSL certificate, received from the server. + /// + /// The callback to get CA certificates. + /// The same builder instance so that multiple calls can be chained. + /// + /// This overload, which accepts a callback, is suitable for scenarios where the certificate rotates + /// and might change during the lifetime of the application. + /// When that's not the case, use the overload which directly accepts the certificate. + /// + /// The same builder instance so that multiple calls can be chained. + public NpgsqlSlimDataSourceBuilder UseRootCertificatesCallback(Func? rootCertificateCallback) + { + _transportSecurityHandler.RootCertificatesCallback = rootCertificateCallback; return this; } diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 7f28aa9e2e..bb06704140 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -22,6 +22,8 @@ Npgsql.NpgsqlDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = nu Npgsql.NpgsqlDataSourceBuilder.MapEnum(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlDataSourceBuilder.ConfigureTracing(System.Action! configureAction) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlDataSourceBuilder.UseNegotiateOptionsCallback(System.Action? negotiateOptionsCallback) -> Npgsql.NpgsqlDataSourceBuilder! +Npgsql.NpgsqlDataSourceBuilder.UseRootCertificates(System.Security.Cryptography.X509Certificates.X509Certificate2Collection? rootCertificates) -> Npgsql.NpgsqlDataSourceBuilder! +Npgsql.NpgsqlDataSourceBuilder.UseRootCertificatesCallback(System.Func? rootCertificateCallback) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlDataSourceBuilder.UseSslClientAuthenticationOptionsCallback(System.Action? sslClientAuthenticationOptionsCallback) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlMetricsOptions Npgsql.NpgsqlMetricsOptions.NpgsqlMetricsOptions() -> void @@ -36,6 +38,8 @@ Npgsql.NpgsqlSlimDataSourceBuilder.MapComposite(string? pgName = null, Npgsql Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.MapEnum(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.UseNegotiateOptionsCallback(System.Action? negotiateOptionsCallback) -> Npgsql.NpgsqlSlimDataSourceBuilder! +Npgsql.NpgsqlSlimDataSourceBuilder.UseRootCertificates(System.Security.Cryptography.X509Certificates.X509Certificate2Collection? rootCertificates) -> Npgsql.NpgsqlSlimDataSourceBuilder! +Npgsql.NpgsqlSlimDataSourceBuilder.UseRootCertificatesCallback(System.Func? rootCertificateCallback) -> Npgsql.NpgsqlSlimDataSourceBuilder! Npgsql.NpgsqlSlimDataSourceBuilder.UseSslClientAuthenticationOptionsCallback(System.Action? sslClientAuthenticationOptionsCallback) -> Npgsql.NpgsqlSlimDataSourceBuilder! *REMOVED*Npgsql.NpgsqlTracingOptions *REMOVED*Npgsql.NpgsqlTracingOptions.NpgsqlTracingOptions() -> void diff --git a/test/Npgsql.Tests/SecurityTests.cs b/test/Npgsql.Tests/SecurityTests.cs index 0ef9fd7b68..c9448b19ee 100644 --- a/test/Npgsql.Tests/SecurityTests.cs +++ b/test/Npgsql.Tests/SecurityTests.cs @@ -2,6 +2,8 @@ using System.IO; using System.Runtime.InteropServices; using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Npgsql.Properties; @@ -563,6 +565,45 @@ public async Task Connect_with_verify_check_host([Values(SslMode.VerifyCA, SslMo } } + [Test] + [Platform(Exclude = "MacOsX", Reason = "Mac requires explicit opt-in to receive CA certificate in TLS handshake")] + public async Task Connect_with_verify_and_multiple_ca_cert([Values(SslMode.VerifyCA, SslMode.VerifyFull)] SslMode sslMode, [Values] bool realCaFirst) + { + if (!IsOnBuildServer) + Assert.Ignore("Only executed in CI"); + + var certificates = new X509Certificate2Collection(); + +#if NET9_0_OR_GREATER + using var realCaCert = X509CertificateLoader.LoadCertificateFromFile("ca.crt"); +#else + using var realCaCert = new X509Certificate2("ca.crt"); +#endif + + using var ecdsa = ECDsa.Create(); + var req = new CertificateRequest("cn=localhost", ecdsa, HashAlgorithmName.SHA256); + using var unrelatedCaCert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); + + if (realCaFirst) + { + certificates.Add(realCaCert); + certificates.Add(unrelatedCaCert); + } + else + { + certificates.Add(unrelatedCaCert); + certificates.Add(realCaCert); + } + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.SslMode = sslMode; + dataSourceBuilder.UseRootCertificates(certificates); + + await using var dataSource = dataSourceBuilder.Build(); + + await using var _ = await dataSource.OpenConnectionAsync(); + } + [Test] [NonParallelizable] // Sets environment variable public async Task Direct_ssl_via_env_requires_correct_sslmode() From ce11aaa601ca7ba4d1974ee59d4ef791f3cb71e0 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 6 Nov 2025 13:39:57 +0300 Subject: [PATCH 119/155] Fix returning properties from NpgsqlConnectionStringBuilder.GetProperties (#6290) Fixes #6289 --- 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 5b2d13a1d0..ca0d734c5f 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -1673,9 +1673,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 33d8cdcd44b99ff1832e653f297a522ebf4170ab Mon Sep 17 00:00:00 2001 From: Kirk Brauer Date: Fri, 7 Nov 2025 14:14:35 -0500 Subject: [PATCH 120/155] Fix the NpgsqlCube to use the full G17 floating-point format (#6295) * Fix the Write() method to use the full G17 floating-point format * Move to one-line namespace --- src/Npgsql/NpgsqlTypes/NpgsqlCube.cs | 416 +++++++++++++-------------- 1 file changed, 208 insertions(+), 208 deletions(-) diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs index 15a89f56ee..24a1ea38b4 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs @@ -1,251 +1,251 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; // ReSharper disable once CheckNamespace -namespace NpgsqlTypes +namespace NpgsqlTypes; + +/// +/// Represents a PostgreSQL cube data type. +/// +/// +/// See https://www.postgresql.org/docs/current/cube.html +/// +public readonly struct NpgsqlCube : IEquatable { + // Store the coordinates as a value tuple array + readonly double[] _lowerLeft; + readonly double[] _upperRight; + /// - /// Represents a PostgreSQl cube data type. + /// The lower left coordinates of the cube. /// - /// - /// See https://www.postgresql.org/docs/current/cube.html - /// - public readonly struct NpgsqlCube : IEquatable + public IReadOnlyList LowerLeft => _lowerLeft; + + /// + /// The upper right coordinates of the cube. + /// + public IReadOnlyList UpperRight => _upperRight; + + /// + /// The number of dimensions of the cube. + /// + public int Dimensions => _lowerLeft.Length; + + /// + /// True if the cube is a point, that is, the two defining corners are the same. + /// + public bool IsPoint { get; } + + /// + /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. + /// + /// This is an internal constructor to optimize the number of allocations. + /// The lower left values. + /// The upper right values. + /// + /// Thrown if the number of dimensions in the upper left and lower right values do not match. + /// + internal NpgsqlCube(double[] lowerLeft, double[] upperRight) { - // Store the coordinates as a value tuple array - readonly double[] _lowerLeft; - readonly double[] _upperRight; - - /// - /// The lower left coordinates of the cube. - /// - public IReadOnlyList LowerLeft => _lowerLeft; - - /// - /// The upper right coordinates of the cube. - /// - public IReadOnlyList UpperRight => _upperRight; - - /// - /// The number of dimensions of the cube. - /// - public int Dimensions => _lowerLeft.Length; - - /// - /// True if the cube is a point, that is, the two defining corners are the same. - /// - public bool IsPoint { get; } - - /// - /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. - /// - /// This is an internal constructor to optimize the number of allocations. - /// The lower left values. - /// The upper right values. - /// - /// Thrown if the number of dimensions in the upper left and lower right values do not match. - /// - internal NpgsqlCube(double[] lowerLeft, double[] upperRight) - { - if (lowerLeft.Length != upperRight.Length) - throw new ArgumentException($"Not a valid cube: Different point dimensions in {lowerLeft} and {upperRight}."); + if (lowerLeft.Length != upperRight.Length) + throw new ArgumentException($"Not a valid cube: Different point dimensions in {lowerLeft} and {upperRight}."); - IsPoint = lowerLeft.SequenceEqual(upperRight); - _lowerLeft = lowerLeft; - _upperRight = upperRight; - } + IsPoint = lowerLeft.SequenceEqual(upperRight); + _lowerLeft = lowerLeft; + _upperRight = upperRight; + } + + /// + /// Makes a one dimensional cube with both coordinates the same. + /// + /// The point coordinate. + public NpgsqlCube(double coord) + { + IsPoint = true; + _lowerLeft = [coord]; + _upperRight = _lowerLeft; + } + + /// + /// Makes a one dimensional cube. + /// + /// The lower left value. + /// The upper right value. + public NpgsqlCube(double lowerLeft, double upperRight) + { + IsPoint = lowerLeft.CompareTo(upperRight) == 0; + _lowerLeft = [lowerLeft]; + _upperRight = IsPoint ? _lowerLeft : [upperRight]; + } + + /// + /// Makes a zero-volume cube using the coordinates defined by the array. + /// + /// The coordinates. + public NpgsqlCube(IEnumerable coords) + { + // Always create a defensive copy to prevent external mutation + _lowerLeft = coords.ToArray(); + IsPoint = true; + _upperRight = _lowerLeft; + } + + /// + /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. + /// + /// The lower left values. + /// The upper right values. + /// + /// Thrown if the number of dimensions in the upper left and lower right values do not match + /// or if the cube exceeds the maximum dimensions (100). + /// + public NpgsqlCube(IEnumerable lowerLeft, IEnumerable upperRight) : + this(lowerLeft.ToArray(), upperRight.ToArray()) + { } - /// - /// Makes a one dimensional cube with both coordinates the same. - /// - /// The point coordinate. - public NpgsqlCube(double coord) + /// + /// Makes a new cube by adding a dimension on to an existing cube, with the same values for both endpoints of the new coordinate. + /// This is useful for building cubes piece by piece from calculated values. + /// + /// The existing cube. + /// The coordinate to add. + public NpgsqlCube(NpgsqlCube cube, double coord) + { + IsPoint = cube.IsPoint; + if (IsPoint) { - IsPoint = true; - _lowerLeft = [coord]; + _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); _upperRight = _lowerLeft; } - - /// - /// Makes a one dimensional cube. - /// - /// The lower left value. - /// The upper right value. - public NpgsqlCube(double lowerLeft, double upperRight) + else { - IsPoint = lowerLeft.CompareTo(upperRight) == 0; - _lowerLeft = [lowerLeft]; - _upperRight = IsPoint ? _lowerLeft : [upperRight]; + _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); + _upperRight = cube._upperRight.Append(coord).ToArray(); } + } - /// - /// Makes a zero-volume cube using the coordinates defined by the array. - /// - /// The coordinates. - public NpgsqlCube(IEnumerable coords) + /// + /// Makes a new cube by adding a dimension on to an existing cube. + /// This is useful for building cubes piece by piece from calculated values. + /// + /// The existing cube. + /// The lower left value. + /// The upper right value. + public NpgsqlCube(NpgsqlCube cube, double lowerLeft, double upperRight) + { + IsPoint = cube.IsPoint && lowerLeft.CompareTo(upperRight) == 0; + if (IsPoint) { - // Always create a defensive copy to prevent external mutation - _lowerLeft = coords.ToArray(); - IsPoint = true; + _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); _upperRight = _lowerLeft; } - - /// - /// Makes a cube with upper right and lower left coordinates as defined by the two arrays, which must be of the same length. - /// - /// The lower left values. - /// The upper right values. - /// - /// Thrown if the number of dimensions in the upper left and lower right values do not match - /// or if the cube exceeds the maximum dimensions (100). - /// - public NpgsqlCube(IEnumerable lowerLeft, IEnumerable upperRight) : - this(lowerLeft.ToArray(), upperRight.ToArray()) - { } - - /// - /// Makes a new cube by adding a dimension on to an existing cube, with the same values for both endpoints of the new coordinate. - /// This is useful for building cubes piece by piece from calculated values. - /// - /// The existing cube. - /// The coordinate to add. - public NpgsqlCube(NpgsqlCube cube, double coord) + else { - IsPoint = cube.IsPoint; - if (IsPoint) - { - _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); - _upperRight = _lowerLeft; - } - else - { - _lowerLeft = cube._lowerLeft.Append(coord).ToArray(); - _upperRight = cube._upperRight.Append(coord).ToArray(); - } + _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); + _upperRight = cube._upperRight.Append(upperRight).ToArray(); } + } + + /// + /// Makes a new cube from an existing cube, using a list of dimension indexes from an array. + /// Can be used to extract the endpoints of a single dimension, or to drop dimensions, or to reorder them as desired. + /// + /// The list of dimension indexes. + /// A new cube. + /// + /// + /// var cube = new NpgsqlCube(new[] { 1, 3, 5 }, new[] { 6, 7, 8 }); // '(1,3,5),(6,7,8)' + /// cube.ToSubset(1); // '(3),(7)' + /// cube.ToSubset(2, 1, 0, 0); // '(5,3,1,1),(8,7,6,6)' + /// + /// + public NpgsqlCube ToSubset(params int[] indexes) + { + var lowerLeft = new double[indexes.Length]; + var upperRight = new double[indexes.Length]; - /// - /// Makes a new cube by adding a dimension on to an existing cube. - /// This is useful for building cubes piece by piece from calculated values. - /// - /// The existing cube. - /// The lower left value. - /// The upper right value. - public NpgsqlCube(NpgsqlCube cube, double lowerLeft, double upperRight) + for (var i = 0; i < indexes.Length; i++) { - IsPoint = cube.IsPoint && lowerLeft.CompareTo(upperRight) == 0; - if (IsPoint) - { - _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); - _upperRight = _lowerLeft; - } - else - { - _lowerLeft = cube._lowerLeft.Append(lowerLeft).ToArray(); - _upperRight = cube._upperRight.Append(upperRight).ToArray(); - } + lowerLeft[i] = _lowerLeft[indexes[i]]; + upperRight[i] = _upperRight[indexes[i]]; } - /// - /// Makes a new cube from an existing cube, using a list of dimension indexes from an array. - /// Can be used to extract the endpoints of a single dimension, or to drop dimensions, or to reorder them as desired. - /// - /// The list of dimension indexes. - /// A new cube. - /// - /// - /// var cube = new NpgsqlCube(new[] { 1, 3, 5 }, new[] { 6, 7, 8 }); // '(1,3,5),(6,7,8)' - /// cube.ToSubset(1); // '(3),(7)' - /// cube.ToSubset(2, 1, 0, 0); // '(5,3,1,1),(8,7,6,6)' - /// - /// - public NpgsqlCube ToSubset(params int[] indexes) - { - var lowerLeft = new double[indexes.Length]; - var upperRight = new double[indexes.Length]; + return new NpgsqlCube(lowerLeft, upperRight); + } - for (var i = 0; i < indexes.Length; i++) - { - lowerLeft[i] = _lowerLeft[indexes[i]]; - upperRight[i] = _upperRight[indexes[i]]; - } + /// + public bool Equals(NpgsqlCube other) => Dimensions == other.Dimensions + && _lowerLeft.SequenceEqual(other._lowerLeft) + && _upperRight.SequenceEqual(other._upperRight); - return new NpgsqlCube(lowerLeft, upperRight); - } + /// + public override bool Equals(object? obj) => obj is NpgsqlCube other && Equals(other); - /// - public bool Equals(NpgsqlCube other) => Dimensions == other.Dimensions - && _lowerLeft.SequenceEqual(other._lowerLeft) - && _upperRight.SequenceEqual(other._upperRight); + /// + public static bool operator ==(NpgsqlCube x, NpgsqlCube y) => x.Equals(y); - /// - public override bool Equals(object? obj) => obj is NpgsqlCube other && Equals(other); + /// + public static bool operator !=(NpgsqlCube x, NpgsqlCube y) => !(x == y); - /// - public static bool operator ==(NpgsqlCube x, NpgsqlCube y) => x.Equals(y); + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + for (var i = 0; i < Dimensions; i++) + { + hashCode.Add(_lowerLeft[i]); + hashCode.Add(_upperRight[i]); + } + return hashCode.ToHashCode(); + } - /// - public static bool operator !=(NpgsqlCube x, NpgsqlCube y) => !(x == y); + /// + /// Writes the cube in PostgreSQL's text format. + /// + void Write(StringBuilder stringBuilder) + { + var leftBuilder = new StringBuilder(); + var rightBuilder = new StringBuilder(); - /// - public override int GetHashCode() + leftBuilder.Append('('); + rightBuilder.Append('('); + + for (var i = 0; i < Dimensions; i++) { - var hashCode = new HashCode(); - for (var i = 0; i < Dimensions; i++) - { - hashCode.Add(_lowerLeft[i]); - hashCode.Add(_upperRight[i]); - } - return hashCode.ToHashCode(); + leftBuilder.Append(_lowerLeft[i].ToString("G17", CultureInfo.InvariantCulture)); + rightBuilder.Append(_upperRight[i].ToString("G17", CultureInfo.InvariantCulture)); + + if (i >= Dimensions - 1) continue; + + leftBuilder.Append(", "); + rightBuilder.Append(", "); } - /// - /// Writes the cube in PostgreSQL's text format. - /// - void Write(StringBuilder stringBuilder) + leftBuilder.Append(')'); + rightBuilder.Append(')'); + + if (IsPoint) { - var leftBuilder = new StringBuilder(); - var rightBuilder = new StringBuilder(); - - leftBuilder.Append('('); - rightBuilder.Append('('); - - for (var i = 0; i < Dimensions; i++) - { - leftBuilder.Append(_lowerLeft[i]); - rightBuilder.Append(_upperRight[i]); - - if (i >= Dimensions - 1) continue; - - leftBuilder.Append(", "); - rightBuilder.Append(", "); - } - - leftBuilder.Append(')'); - rightBuilder.Append(')'); - - if (IsPoint) - { - stringBuilder.Append(leftBuilder); - } - else - { - stringBuilder.Append(leftBuilder); - stringBuilder.Append(','); - stringBuilder.Append(rightBuilder); - } + stringBuilder.Append(leftBuilder); } - - /// - /// Writes the cube in PostgreSQL's text format. - /// - public override string ToString() + else { - var sb = new StringBuilder(); - Write(sb); - return sb.ToString(); + stringBuilder.Append(leftBuilder); + stringBuilder.Append(','); + stringBuilder.Append(rightBuilder); } } + + /// + /// Writes the cube in PostgreSQL's text format. + /// + public override string ToString() + { + var sb = new StringBuilder(); + Write(sb); + return sb.ToString(); + } } From f9ac0b7358ad2cfb0d4c8b1a3a20032b517fed65 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sat, 8 Nov 2025 17:13:26 +0100 Subject: [PATCH 121/155] Remove intermediate string allocations from NpgsqlCube.Write --- src/Npgsql/NpgsqlTypes/NpgsqlCube.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs index 24a1ea38b4..b84953c483 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlCube.cs @@ -215,8 +215,8 @@ void Write(StringBuilder stringBuilder) for (var i = 0; i < Dimensions; i++) { - leftBuilder.Append(_lowerLeft[i].ToString("G17", CultureInfo.InvariantCulture)); - rightBuilder.Append(_upperRight[i].ToString("G17", CultureInfo.InvariantCulture)); + leftBuilder.Append(CultureInfo.InvariantCulture, $"{_lowerLeft[i]:G17}"); + rightBuilder.Append(CultureInfo.InvariantCulture, $"{_upperRight[i]:G17}"); if (i >= Dimensions - 1) continue; From b3f4f3efeb9d1b7385a759138b4259c4cfa9a252 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sat, 8 Nov 2025 18:10:23 +0100 Subject: [PATCH 122/155] Include .NET 10.0 for the main project and all tests (#6300) --- .github/workflows/native-aot.yml | 12 ++++++------ Directory.Packages.props | 1 - src/Npgsql/Npgsql.csproj | 2 +- test/Directory.Build.props | 2 +- test/MStatDumper/MStatDumper.csproj | 2 -- test/Npgsql.Tests/Npgsql.Tests.csproj | 1 - 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index ef6d7b96b5..3cb3ab007a 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -88,8 +88,8 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04 ] - pg_major: [ 15 ] - tfm: [ net9.0 ] + pg_major: [ 18 ] + tfm: [ net10.0 ] steps: - name: Checkout @@ -122,8 +122,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-24.04] - pg_major: [15] - tfm: [ net9.0 ] + pg_major: [ 18 ] + tfm: [ net10.0 ] steps: - name: Checkout @@ -163,11 +163,11 @@ jobs: - name: Write binary size to summary run: | - size="$(ls -l test/Npgsql.NativeAotTests/bin/Release/net9.0/linux-x64/native/Npgsql.NativeAotTests | cut -d ' ' -f 5)" + size="$(ls -l test/Npgsql.NativeAotTests/bin/Release/${{ matrix.tfm }}/linux-x64/native/Npgsql.NativeAotTests | cut -d ' ' -f 5)" echo "Binary size is $size bytes ($((size / (1024 * 1024))) mb)" >> $GITHUB_STEP_SUMMARY - name: Dump mstat - run: dotnet run --project test/MStatDumper/MStatDumper.csproj -c release -f ${{ matrix.tfm }} -- "test/Npgsql.NativeAotTests/obj/Release/net9.0/linux-x64/native/Npgsql.NativeAotTests.mstat" md >> $GITHUB_STEP_SUMMARY + run: dotnet run --project test/MStatDumper/MStatDumper.csproj -c release -f ${{ matrix.tfm }} -- "test/Npgsql.NativeAotTests/obj/Release/${{ matrix.tfm }}/linux-x64/native/Npgsql.NativeAotTests.mstat" md >> $GITHUB_STEP_SUMMARY - name: Upload mstat uses: actions/upload-artifact@v5 diff --git a/Directory.Packages.props b/Directory.Packages.props index 758397b7cd..6864076aef 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,6 @@ - diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 80c42ba561..5273736ceb 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -5,7 +5,7 @@ Npgsql is the open source .NET data provider for PostgreSQL. npgsql;postgresql;postgres;ado;ado.net;database;sql README.md - net8.0;net9.0 + net8.0;net9.0;net10.0 $(NoWarn);CA2017 $(NoWarn);NPG9001 $(NoWarn);NPG9002 diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 1e2132817e..0c3bd8dba0 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - net8.0;net9.0 + net8.0;net10.0 false diff --git a/test/MStatDumper/MStatDumper.csproj b/test/MStatDumper/MStatDumper.csproj index 456bd1f3b9..6405431678 100644 --- a/test/MStatDumper/MStatDumper.csproj +++ b/test/MStatDumper/MStatDumper.csproj @@ -2,8 +2,6 @@ Exe - - net9.0 enable disable diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 1c300f8215..d2da055a6e 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -3,7 +3,6 @@ - all From 6714f65997fc50410cf82f3e82a201bf58b0014b Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Sun, 9 Nov 2025 14:07:10 +0100 Subject: [PATCH 123/155] Prioritize _dataTypeName over _npgsqlDbType (#6299) --- src/Npgsql/NpgsqlParameter.cs | 8 ++++---- test/Npgsql.Tests/NpgsqlParameterTests.cs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index ba2d840925..befef4cef2 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -533,10 +533,10 @@ internal void ResolveTypeInfo(PgSerializerOptions options) if (!previouslyResolved) { var dataTypeName = - _npgsqlDbType is { } npgsqlDbType - ? npgsqlDbType.ToDataTypeName() ?? npgsqlDbType.ToUnqualifiedDataTypeNameOrThrow() - : _dataTypeName is not null - ? Internal.Postgres.DataTypeName.NormalizeName(_dataTypeName) + _dataTypeName is not null + ? Internal.Postgres.DataTypeName.NormalizeName(_dataTypeName) + : _npgsqlDbType is { } npgsqlDbType + ? npgsqlDbType.ToDataTypeName() ?? npgsqlDbType.ToUnqualifiedDataTypeNameOrThrow() : null; PgTypeId? pgTypeId = null; diff --git a/test/Npgsql.Tests/NpgsqlParameterTests.cs b/test/Npgsql.Tests/NpgsqlParameterTests.cs index 4965491c82..cab1047015 100644 --- a/test/Npgsql.Tests/NpgsqlParameterTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterTests.cs @@ -750,6 +750,26 @@ public void Changing_value_type_reresolves([Values]bool generic) Assert.That(thirdTypeInfo, Is.Not.SameAs(typeInfo)); } + [Test] + public void DataTypeName_prioritized_over_NpgsqlDbType([Values]bool generic) + { + var param = generic ? new NpgsqlParameter + { + NpgsqlDbType = NpgsqlDbType.Integer, + DataTypeName = "text", + Value = "value" + } : new NpgsqlParameter + { + NpgsqlDbType = NpgsqlDbType.Integer, + DataTypeName = "text", + Value = "value" + }; + param.ResolveTypeInfo(DataSource.SerializerOptions); + param.GetResolutionInfo(out var typeInfo, out _, out _); + Assert.That(typeInfo, Is.Not.Null); + Assert.That(typeInfo.PgTypeId, Is.EqualTo(DataSource.SerializerOptions.TextPgTypeId)); + } + #if NeedsPorting [Test] [Category ("NotWorking")] From 82a849fd163df617ad791c140e3ab0de6be3ea77 Mon Sep 17 00:00:00 2001 From: lfpraca <134505289+lfpraca@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:56:31 -0300 Subject: [PATCH 124/155] Do not parse batch commands with no parameters (#6298) --- src/Npgsql/NpgsqlCommand.cs | 5 +++++ test/Npgsql.Tests/BatchTests.cs | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 8f6816b657..41f2198a27 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -911,6 +911,11 @@ internal void ProcessRawQuery(SqlQueryParser? parser, bool standardConformingStr break; case PlaceholderType.NoParameters: + if (batchCommand is not null) + { + batchCommand.FinalCommandText = batchCommand.CommandText; + break; + } // Unless the EnableSqlRewriting AppContext switch is explicitly disabled, queries with no parameters are parsed just // like queries with named parameters, since they may contain a semicolon (legacy batching). if (EnableSqlRewriting) diff --git a/test/Npgsql.Tests/BatchTests.cs b/test/Npgsql.Tests/BatchTests.cs index 837ace48fd..d1df99faca 100644 --- a/test/Npgsql.Tests/BatchTests.cs +++ b/test/Npgsql.Tests/BatchTests.cs @@ -669,7 +669,7 @@ public async Task Empty_batch() } [Test] - public async Task Semicolon_is_not_allowed() + public async Task Semicolon_is_not_allowed_with_no_parameters() { await using var conn = await OpenConnectionAsync(); await using var batch = new NpgsqlBatch(conn) @@ -677,6 +677,24 @@ public async Task Semicolon_is_not_allowed() BatchCommands = { new("SELECT 1; SELECT 2") } }; + Assert.That(() => batch.ExecuteReaderAsync(Behavior), Throws.Exception.TypeOf()); + } + + [Test] + public async Task Semicolon_is_not_allowed_with_named_parameters() + { + await using var conn = await OpenConnectionAsync(); + await using var batch = new NpgsqlBatch(conn) + { + BatchCommands = + { + new("SELECT @p1; SELECT 2") + { + Parameters = { new("p1", 1) } + } + } + }; + Assert.That(() => batch.ExecuteReaderAsync(Behavior), Throws.Exception.TypeOf()); } From ea5a2e189a71fee7340684d9ba51ba8a38d8a35f Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 10 Nov 2025 14:01:42 +0100 Subject: [PATCH 125/155] Improve output parameter handling (#5645) Closes #2252 --- src/Npgsql/NpgsqlBatchCommand.cs | 109 ++++++++++++++++++++++++++++++ src/Npgsql/NpgsqlDataReader.cs | 79 ++++++++-------------- src/Npgsql/NpgsqlParameter.cs | 10 +++ src/Npgsql/NpgsqlParameter`.cs | 3 + test/Npgsql.Tests/CommandTests.cs | 46 ++++++++++++- 5 files changed, 194 insertions(+), 53 deletions(-) diff --git a/src/Npgsql/NpgsqlBatchCommand.cs b/src/Npgsql/NpgsqlBatchCommand.cs index c1cfa3ef87..17cec381b2 100644 --- a/src/Npgsql/NpgsqlBatchCommand.cs +++ b/src/Npgsql/NpgsqlBatchCommand.cs @@ -1,10 +1,12 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; using Npgsql.BackendMessages; using Npgsql.Internal; @@ -41,6 +43,7 @@ public override string CommandText /// public new NpgsqlParameterCollection Parameters => _parameters ??= []; + internal bool HasOutputParameters => _parameters?.HasOutputParameters == true; /// public override NpgsqlParameter CreateParameter() => new(); @@ -273,6 +276,112 @@ internal void ApplyCommandComplete(CommandCompleteMessage msg) internal void ResetPreparation() => ConnectorPreparedOn = null; + internal void PopulateOutputParameters(NpgsqlDataReader reader, ILogger logger) + { + Debug.Assert(_parameters is not null); + var parameters = _parameters; + var fieldCount = reader.FieldCount; + switch (parameters.PlaceholderType) + { + case PlaceholderType.Mixed: + case PlaceholderType.Named: + { + // In the case of named and mixed parameters we first try to populate all parameters with a named column match. + // For backwards compat we allow populating named parameters as long as they haven't been filled yet. + // So for every column that we couldn't match by name we fill the first output direction parameter that wasn't filled previously. + // This means a row like {"a" => 1, "some_field" => 2} will populate the following output db params {"a" => 1, "b" => 2}. + // And a row like {"some_field" => 1, "a" => 2} will populate them as follows {"a" => 2, "b" => 1}. + + var parameterIndices = new ArraySegment(ArrayPool.Shared.Rent(fieldCount), 0, fieldCount); + var secondPassOrdinal = -1; + for (var ordinal = 0; ordinal < fieldCount; ordinal++) + { + var name = reader.GetName(ordinal); + var i = parameters.IndexOf(name); + if (i is not -1 && parameters[i] is { IsOutputDirection: true } parameter) + { + SetValue(reader, logger, parameter, ordinal, i); + parameterIndices[ordinal] = i; + } + else + { + parameterIndices[ordinal] = -1; + if (secondPassOrdinal is -1) + secondPassOrdinal = ordinal; + } + } + + if (secondPassOrdinal is -1) + { + ArrayPool.Shared.Return(parameterIndices.Array!); + break; + } + + // This set will also contain -1, but that's not a valid index so we can ignore it is included. + var matchedParameters = new HashSet(parameterIndices); + var parameterList = parameters.InternalList; + for (var i = 0; i < parameterList.Count; i++) + { + // Find an output parameter that wasn't matched by name. + if (parameterList[i] is not { IsOutputDirection: true } parameter || matchedParameters.Contains(i)) + continue; + + SetValue(reader, logger, parameter, secondPassOrdinal, i); + + // And find the next unhandled ordinal. + secondPassOrdinal = NextSecondPassOrdinal(parameterIndices, secondPassOrdinal); + if (secondPassOrdinal is -1) + break; + } + + ArrayPool.Shared.Return(parameterIndices.Array!); + break; + + static int NextSecondPassOrdinal(ArraySegment indices, int offset) + { + for (var i = offset + 1; i < indices.Count; i++) + { + if (indices[i] is -1) + return i; + } + + return -1; + } + } + case PlaceholderType.Positional: + { + var parameterList = parameters.InternalList; + var ordinal = 0; + for (var i = 0; i < parameterList.Count; i++) + { + if (parameterList[i] is not { IsOutputDirection: true } parameter) + continue; + + SetValue(reader, logger, parameter, ordinal, i); + + ordinal++; + if (ordinal == fieldCount) + break; + } + break; + } + } + + static void SetValue(NpgsqlDataReader reader, ILogger logger, NpgsqlParameter p, int ordinal, int index) + { + try + { + p.SetOutputValue(reader, ordinal); + } + catch (Exception ex) + { + logger.LogDebug(ex, "Failed to set value on output parameter instance '{ParameterNameOrIndex}' for output parameter {OutputName}", + p.ParameterName is NpgsqlParameter.PositionalName ? index : p.ParameterName, reader.GetName(ordinal)); + throw; + } + } + } + /// /// Returns the . /// diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 585536da18..2421e7e5ca 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -459,18 +459,44 @@ async Task NextResult(bool async, bool isConsuming = false, CancellationTo continue; } - if ((Command.WrappingBatch is not null || StatementIndex is 0) && Command.InternalBatchCommands[StatementIndex]._parameters?.HasOutputParameters == true) + if ((Command.WrappingBatch is not null || StatementIndex is 0) && Command.InternalBatchCommands[StatementIndex] is { HasOutputParameters: true } command) { // If output parameters are present and this is the first row of the resultset, // we must always read it in non-sequential mode because it will be traversed twice (once // here for the parameters, then as a regular row). - msg = await Connector.ReadMessage(async).ConfigureAwait(false); + msg = await Connector.ReadMessage(async, dataRowLoadingMode: DataRowLoadingMode.NonSequential).ConfigureAwait(false); ProcessMessage(msg); if (msg.Code == BackendMessageCode.DataRow) { + Debug.Assert(RowDescription != null); + Debug.Assert(State == ReaderState.BeforeResult); + try { - PopulateOutputParameters(Command.InternalBatchCommands[StatementIndex]._parameters!); + // Temporarily set our state to InResult and non-sequential to allow us to read the values, and in any order. + var isSequential = _isSequential; + var currentPosition = Buffer.ReadPosition; + State = ReaderState.InResult; + _isSequential = false; + try + { + command.PopulateOutputParameters(this, _commandLogger); + + // On success we want to revert any row and column state for the user to be able to read the same row again. + if (async) + await PgReader.CommitAsync().ConfigureAwait(false); + else + PgReader.Commit(); + + State = ReaderState.BeforeResult; // Set the state back + Buffer.ReadPosition = currentPosition; // Restore position + _column = -1; + } + finally + { + // To be on the safe side we always revert this CommandBehavior state change, including on failure. + _isSequential = isSequential; + } } catch (Exception e) { @@ -612,53 +638,6 @@ async ValueTask ConsumeResultSet(bool async) } } - - void PopulateOutputParameters(NpgsqlParameterCollection parameters) - { - // The first row in a stored procedure command that has output parameters needs to be traversed twice - - // once for populating the output parameters and once for the actual result set traversal. So in this - // case we can't be sequential. - Debug.Assert(RowDescription != null); - Debug.Assert(State == ReaderState.BeforeResult); - - var currentPosition = Buffer.ReadPosition; - - // Temporarily set our state to InResult to allow us to read the values - State = ReaderState.InResult; - - var pending = new Queue(); - var taken = new List(); - for (var i = 0; i < ColumnCount; i++) - { - if (parameters.TryGetValue(GetName(i), out var p) && p.IsOutputDirection) - { - p.Value = GetValue(i); - taken.Add(p); - } - else - pending.Enqueue(GetValue(i)); - } - - // Not sure where this odd behavior comes from: all output parameters which did not get matched by - // name now get populated with column values which weren't matched. Keeping this for backwards compat, - // opened #2252 for investigation. - foreach (var p in (IEnumerable)parameters) - { - if (!p.IsOutputDirection || taken.Contains(p)) - continue; - - if (pending.Count == 0) - break; - p.Value = pending.Dequeue(); - } - - PgReader.Commit(); - State = ReaderState.BeforeResult; // Set the state back - Buffer.ReadPosition = currentPosition; // Restore position - - _column = -1; - } - /// /// Note that in SchemaOnly mode there are no resultsets, and we read nothing from the backend (all /// RowDescriptions have already been processed and are available) diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index befef4cef2..ca7ec17cbb 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -497,6 +497,16 @@ public sealed override string SourceColumn Type? GetValueType(Type staticValueType) => staticValueType != typeof(object) ? staticValueType : Value?.GetType(); + internal void SetOutputValue(NpgsqlDataReader reader, int ordinal) + { + if (GetType() == typeof(NpgsqlParameter)) + Value = reader.GetValue(ordinal); + else + SetOutputValueCore(reader, ordinal); + } + + private protected virtual void SetOutputValueCore(NpgsqlDataReader reader, int ordinal) {} + internal bool ShouldResetObjectTypeInfo(object? value) { var currentType = TypeInfo?.Type; diff --git a/src/Npgsql/NpgsqlParameter`.cs b/src/Npgsql/NpgsqlParameter`.cs index e50618a510..d353cdce45 100644 --- a/src/Npgsql/NpgsqlParameter`.cs +++ b/src/Npgsql/NpgsqlParameter`.cs @@ -81,6 +81,9 @@ public NpgsqlParameter(string parameterName, DbType dbType) #endregion Constructors + private protected override void SetOutputValueCore(NpgsqlDataReader reader, int ordinal) + => TypedValue = reader.GetFieldValue(ordinal); + private protected override PgConverterResolution ResolveConverter(PgTypeInfo typeInfo) { if (typeof(T) == typeof(object) || TypeInfo!.IsBoxing) diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs index a5fb272851..584a3cc433 100644 --- a/test/Npgsql.Tests/CommandTests.cs +++ b/test/Npgsql.Tests/CommandTests.cs @@ -741,14 +741,14 @@ public async Task Cached_command_clears_parameters_placeholder_type() public async Task Statement_mapped_output_parameters(CommandBehavior behavior) { await using var conn = await OpenConnectionAsync(); - var command = new NpgsqlCommand("select 3, 4 as param1, 5 as param2, 6;", conn); + var command = new NpgsqlCommand("select 3 as unknown, 4 as param1, 5 as param2, 6;", conn); - var p = new NpgsqlParameter("param2", NpgsqlDbType.Integer); + var p = new NpgsqlParameter("param1", NpgsqlDbType.Integer); p.Direction = ParameterDirection.Output; p.Value = -1; command.Parameters.Add(p); - p = new NpgsqlParameter("param1", NpgsqlDbType.Integer); + p = new NpgsqlParameter("param2", NpgsqlDbType.Integer); p.Direction = ParameterDirection.Output; p.Value = -1; command.Parameters.Add(p); @@ -760,6 +760,7 @@ public async Task Statement_mapped_output_parameters(CommandBehavior behavior) await using var reader = await command.ExecuteReaderAsync(behavior); + Assert.That(command.Parameters["p"].Value, Is.EqualTo(3)); Assert.That(command.Parameters["param1"].Value, Is.EqualTo(4)); Assert.That(command.Parameters["param2"].Value, Is.EqualTo(5)); @@ -771,6 +772,45 @@ public async Task Statement_mapped_output_parameters(CommandBehavior behavior) Assert.That(reader.GetInt32(3), Is.EqualTo(6)); } + + [Test] + [TestCase(CommandBehavior.Default)] + [TestCase(CommandBehavior.SequentialAccess)] + public async Task Statement_mapped_generic_output_parameters(CommandBehavior behavior) + { + await using var conn = await OpenConnectionAsync(); + var command = new NpgsqlCommand("select '' as unknown, 4 as param1, 5 as param2, 6;", conn); + + var p = new NpgsqlParameter("param1", NpgsqlDbType.Integer); + p.Direction = ParameterDirection.Output; + p.Value = -1; + command.Parameters.Add(p); + + p = new NpgsqlParameter("param2", NpgsqlDbType.Integer); + p.Direction = ParameterDirection.Output; + p.Value = -1; + command.Parameters.Add(p); + + // char[] is one alternative mapping for text. + var textP = new NpgsqlParameter("p", NpgsqlDbType.Text); + textP.Direction = ParameterDirection.Output; + textP.Value = "text".ToCharArray(); + command.Parameters.Add(textP); + + await using var reader = await command.ExecuteReaderAsync(behavior); + + Assert.That(command.Parameters["p"].Value, Is.EquivalentTo(Array.Empty())); + Assert.That(command.Parameters["param1"].Value, Is.EqualTo(4)); + Assert.That(command.Parameters["param2"].Value, Is.EqualTo(5)); + + reader.Read(); + + Assert.That(reader.GetFieldValue(0), Is.EquivalentTo(Array.Empty())); + Assert.That(reader.GetInt32(1), Is.EqualTo(4)); + Assert.That(reader.GetInt32(2), Is.EqualTo(5)); + Assert.That(reader.GetInt32(3), Is.EqualTo(6)); + } + [Test] public async Task Bug1006158_output_parameters() { From 53030fb432b246c37d9ed0c85d6102f509f5ea75 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Mon, 10 Nov 2025 23:55:10 +0100 Subject: [PATCH 126/155] Check pgTypeId for default resolution call in ObjectConverter (#6304) --- src/Npgsql/Internal/Converters/ObjectConverter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Npgsql/Internal/Converters/ObjectConverter.cs b/src/Npgsql/Internal/Converters/ObjectConverter.cs index 394f7a8a4c..4889c60fad 100644 --- a/src/Npgsql/Internal/Converters/ObjectConverter.cs +++ b/src/Npgsql/Internal/Converters/ObjectConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Npgsql.Internal.Postgres; @@ -37,6 +38,7 @@ public override Size GetSize(SizeContext context, object value, ref object? writ // We can call GetDefaultResolution here as validation has already happened in IsDbNullValue. // And we know it was called due to the writeState being filled. + Debug.Assert(typeInfo.PgTypeId is not null); var converter = typeInfo is PgResolverTypeInfo resolverTypeInfo ? resolverTypeInfo.GetDefaultResolution(null).Converter : typeInfo.GetResolution().Converter; @@ -79,6 +81,7 @@ async ValueTask Write(bool async, PgWriter writer, object value, CancellationTok // We can call GetDefaultResolution here as validation has already happened in IsDbNullValue. // And we know it was called due to the writeState being filled. + Debug.Assert(typeInfo.PgTypeId is not null); var converter = typeInfo is PgResolverTypeInfo resolverTypeInfo ? resolverTypeInfo.GetDefaultResolution(null).Converter : typeInfo.GetResolution().Converter; From dcbb5c29964ba07bd7268fe2ec9d73aa64066317 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Tue, 11 Nov 2025 21:13:36 +0100 Subject: [PATCH 127/155] Move all reloadable state into one reference (#6303) --- src/Npgsql/Internal/NpgsqlConnector.cs | 12 +++--- src/Npgsql/NpgsqlCommand.cs | 18 +++++---- src/Npgsql/NpgsqlDataSource.cs | 47 ++++++++++++++--------- src/Npgsql/NpgsqlParameterCollection.cs | 5 +-- src/Npgsql/PoolingDataSource.cs | 8 +--- test/Npgsql.Benchmarks/ResolveHandler.cs | 2 +- test/Npgsql.Tests/ConnectionTests.cs | 23 +++++------ test/Npgsql.Tests/NpgsqlParameterTests.cs | 16 ++++---- test/Npgsql.Tests/PostgresTypeTests.cs | 2 +- 9 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 40dcf6e941..bdd82c761f 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -117,12 +117,13 @@ internal string InferredUserName /// internal int Id => BackendProcessId; - internal PgSerializerOptions SerializerOptions { get; set; } = default!; + internal NpgsqlDataSource.ReloadableState ReloadableState = null!; /// /// Information about PostgreSQL and PostgreSQL-like databases (e.g. type definitions, capabilities...). /// - public NpgsqlDatabaseInfo DatabaseInfo { get; internal set; } = default!; + public NpgsqlDatabaseInfo DatabaseInfo => ReloadableState.DatabaseInfo; + internal PgSerializerOptions SerializerOptions => ReloadableState.SerializerOptions; /// /// The current transaction status for this connector. @@ -507,10 +508,9 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca await DataSource.Bootstrap(this, timeout, forceReload: false, async, cancellationToken).ConfigureAwait(false); - Debug.Assert(DataSource.SerializerOptions is not null); - Debug.Assert(DataSource.DatabaseInfo is not null); - SerializerOptions = DataSource.SerializerOptions; - DatabaseInfo = DataSource.DatabaseInfo; + // The connector directly references the current reloadable state reference, to protect it against changes by a concurrent + // ReloadTypes. We update them here before returning the connector from the pool. + ReloadableState = DataSource.CurrentReloadableState; if (Settings.Pooling && Settings is { Multiplexing: false, NoResetOnClose: false } && DatabaseInfo.SupportsDiscard) { diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 41f2198a27..5f2cb8832f 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -671,7 +671,7 @@ Task Prepare(bool async, CancellationToken cancellationToken = default) { foreach (var batchCommand in InternalBatchCommands) { - batchCommand._parameters?.ProcessParameters(connector.SerializerOptions, validateValues: false, batchCommand.CommandType); + batchCommand._parameters?.ProcessParameters(connector.ReloadableState, validateValues: false, batchCommand.CommandType); ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand); needToPrepare = batchCommand.ExplicitPrepare(connector) || needToPrepare; @@ -689,7 +689,7 @@ IEnumerable CommandTexts() } else { - _parameters?.ProcessParameters(connector.SerializerOptions, validateValues: false, CommandType); + _parameters?.ProcessParameters(connector.ReloadableState, validateValues: false, CommandType); ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand: null); foreach (var batchCommand in InternalBatchCommands) @@ -1410,6 +1410,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com if (connector is not null) { var logger = connector.CommandLogger; + var reloadableState = connector.ReloadableState; cancellationToken.ThrowIfCancellationRequested(); // We cannot pass a token here, as we'll cancel a non-send query @@ -1440,7 +1441,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com goto case false; } - batchCommand._parameters?.ProcessParameters(connector.SerializerOptions, validateParameterValues, batchCommand.CommandType); + batchCommand._parameters?.ProcessParameters(reloadableState, validateParameterValues, batchCommand.CommandType); } } else @@ -1453,7 +1454,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com ResetPreparation(); goto case false; } - _parameters?.ProcessParameters(connector.SerializerOptions, validateParameterValues, CommandType); + _parameters?.ProcessParameters(reloadableState, validateParameterValues, CommandType); } NpgsqlEventSource.Log.CommandStartPrepared(); @@ -1469,7 +1470,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com { var batchCommand = InternalBatchCommands[i]; - batchCommand._parameters?.ProcessParameters(connector.SerializerOptions, validateParameterValues, batchCommand.CommandType); + batchCommand._parameters?.ProcessParameters(reloadableState, validateParameterValues, batchCommand.CommandType); ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand); if (connector.Settings.MaxAutoPrepare > 0 && batchCommand.TryAutoPrepare(connector)) @@ -1481,7 +1482,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com } else { - _parameters?.ProcessParameters(connector.SerializerOptions, validateParameterValues, CommandType); + _parameters?.ProcessParameters(reloadableState, validateParameterValues, CommandType); ProcessRawQuery(connector.SqlQueryParser, connector.UseConformingStrings, batchCommand: null); if (connector.Settings.MaxAutoPrepare > 0) @@ -1565,6 +1566,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com // The connection isn't bound to a connector - it's multiplexing time. var dataSource = (MultiplexingDataSource)conn.NpgsqlDataSource; + var reloadableState = dataSource.CurrentReloadableState; if (!async) { @@ -1577,13 +1579,13 @@ internal virtual async ValueTask ExecuteReader(bool async, Com { foreach (var batchCommand in InternalBatchCommands) { - batchCommand._parameters?.ProcessParameters(dataSource.SerializerOptions, validateValues: true, batchCommand.CommandType); + batchCommand._parameters?.ProcessParameters(reloadableState, validateValues: true, batchCommand.CommandType); ProcessRawQuery(null, standardConformingStrings: true, batchCommand); } } else { - _parameters?.ProcessParameters(dataSource.SerializerOptions, validateValues: true, CommandType); + _parameters?.ProcessParameters(reloadableState, validateValues: true, CommandType); ProcessRawQuery(null, standardConformingStrings: true, batchCommand: null); } diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 080906ca26..51042bb654 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -31,12 +31,19 @@ public abstract class NpgsqlDataSource : DbDataSource internal NpgsqlLoggingConfiguration LoggingConfiguration { get; } readonly PgTypeInfoResolverChain _resolverChain; - internal PgSerializerOptions SerializerOptions { get; private set; } = null!; // Initialized at bootstrapping - /// - /// Information about PostgreSQL and PostgreSQL-like databases (e.g. type definitions, capabilities...). - /// - internal NpgsqlDatabaseInfo DatabaseInfo { get; private set; } = null!; // Initialized at bootstrapping + internal ReloadableState CurrentReloadableState = null!; // Initialized during bootstrapping. + + // Initialized at bootstrapping + internal sealed class ReloadableState(NpgsqlDatabaseInfo databaseInfo, PgSerializerOptions serializerOptions) + { + /// + /// Information about PostgreSQL and PostgreSQL-like databases (e.g. type definitions, capabilities...). + /// + public NpgsqlDatabaseInfo DatabaseInfo { get; } = databaseInfo; + + public PgSerializerOptions SerializerOptions { get; } = serializerOptions; + } internal TransportSecurityHandler TransportSecurityHandler { get; } @@ -105,7 +112,7 @@ internal NpgsqlDataSource( _periodicPasswordProvider, _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval, - var resolverChain, + _resolverChain, _defaultNameTranslator, ConnectionInitializer, ConnectionInitializerAsync, @@ -115,7 +122,6 @@ internal NpgsqlDataSource( Debug.Assert(_passwordProvider is null || _passwordProviderAsync is not null); - _resolverChain = resolverChain; _password = settings.Password; if (_periodicPasswordSuccessRefreshInterval != default) @@ -272,27 +278,30 @@ internal async Task Bootstrap( // The type loading below will need to send queries to the database, and that depends on a type mapper being set up (even if its // empty). So we set up a minimal version here, and then later inject the actual DatabaseInfo. - connector.SerializerOptions = - new(PostgresMinimalDatabaseInfo.DefaultTypeCatalog) + connector.ReloadableState = new( + databaseInfo: PostgresMinimalDatabaseInfo.DefaultTypeCatalog, + serializerOptions: new(PostgresMinimalDatabaseInfo.DefaultTypeCatalog) { TextEncoding = connector.TextEncoding, TypeInfoResolver = AdoTypeInfoResolverFactory.Instance.CreateResolver(), - }; + }); NpgsqlDatabaseInfo databaseInfo; using (connector.StartUserAction(ConnectorState.Executing, cancellationToken)) databaseInfo = await NpgsqlDatabaseInfo.Load(connector, timeout, async).ConfigureAwait(false); - connector.DatabaseInfo = DatabaseInfo = databaseInfo; - connector.SerializerOptions = SerializerOptions = - new(databaseInfo, _resolverChain, CreateTimeZoneProvider(connector.Timezone)) - { - ArrayNullabilityMode = Settings.ArrayNullabilityMode, - EnableDateTimeInfinityConversions = !Statics.DisableDateTimeInfinityConversions, - TextEncoding = connector.TextEncoding, - DefaultNameTranslator = _defaultNameTranslator - }; + var serializerOptions = new PgSerializerOptions(databaseInfo, _resolverChain, CreateTimeZoneProvider(connector.Timezone)) + { + ArrayNullabilityMode = Settings.ArrayNullabilityMode, + EnableDateTimeInfinityConversions = !Statics.DisableDateTimeInfinityConversions, + TextEncoding = connector.TextEncoding, + DefaultNameTranslator = _defaultNameTranslator + }; + + connector.ReloadableState = CurrentReloadableState = new ReloadableState( + databaseInfo: databaseInfo, + serializerOptions: serializerOptions); IsBootstrapped = true; } diff --git a/src/Npgsql/NpgsqlParameterCollection.cs b/src/Npgsql/NpgsqlParameterCollection.cs index 106f681f0b..17e7fb7969 100644 --- a/src/Npgsql/NpgsqlParameterCollection.cs +++ b/src/Npgsql/NpgsqlParameterCollection.cs @@ -5,7 +5,6 @@ using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Npgsql.Internal; using NpgsqlTypes; namespace Npgsql; @@ -668,7 +667,7 @@ internal void CloneTo(NpgsqlParameterCollection other) } } - internal void ProcessParameters(PgSerializerOptions options, bool validateValues, CommandType commandType) + internal void ProcessParameters(NpgsqlDataSource.ReloadableState reloadableState, bool validateValues, CommandType commandType) { HasOutputParameters = false; PlaceholderType = PlaceholderType.NoParameters; @@ -725,7 +724,7 @@ internal void ProcessParameters(PgSerializerOptions options, bool validateValues break; } - p.ResolveTypeInfo(options); + p.ResolveTypeInfo(reloadableState.SerializerOptions); if (validateValues) { diff --git a/src/Npgsql/PoolingDataSource.cs b/src/Npgsql/PoolingDataSource.cs index a6c63494a8..b2e96d0d4c 100644 --- a/src/Npgsql/PoolingDataSource.cs +++ b/src/Npgsql/PoolingDataSource.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using System.Transactions; using Microsoft.Extensions.Logging; using Npgsql.Internal; using Npgsql.Util; @@ -239,12 +238,9 @@ bool CheckIdleConnector([NotNullWhen(true)] NpgsqlConnector? connector) return false; } - // The connector directly references the data source type mapper into the connector, to protect it against changes by a concurrent + // The connector directly references the current reloadable state reference, to protect it against changes by a concurrent // ReloadTypes. We update them here before returning the connector from the pool. - Debug.Assert(SerializerOptions is not null); - Debug.Assert(DatabaseInfo is not null); - connector.SerializerOptions = SerializerOptions; - connector.DatabaseInfo = DatabaseInfo; + connector.ReloadableState = CurrentReloadableState; Debug.Assert(connector.State == ConnectorState.Ready, $"Got idle connector but {nameof(connector.State)} is {connector.State}"); diff --git a/test/Npgsql.Benchmarks/ResolveHandler.cs b/test/Npgsql.Benchmarks/ResolveHandler.cs index e082b81c4e..ead3a547ed 100644 --- a/test/Npgsql.Benchmarks/ResolveHandler.cs +++ b/test/Npgsql.Benchmarks/ResolveHandler.cs @@ -22,7 +22,7 @@ public void Setup() if (NumPlugins > 1) dataSourceBuilder.UseNetTopologySuite(); _dataSource = dataSourceBuilder.Build(); - _serializerOptions = _dataSource.SerializerOptions; + _serializerOptions = _dataSource.CurrentReloadableState.SerializerOptions; } [GlobalCleanup] diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 7ef94350fd..e64b3ba982 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -431,7 +431,7 @@ public async Task Timezone_connection_param() public async Task Application_name_env_var() { const string testAppName = "MyTestApp"; - + // Note that the pool is unaware of the environment variable, so if a connection is // returned from the pool it may contain the wrong application name using var _ = SetEnvironmentVariable("PGAPPNAME", testAppName); @@ -444,7 +444,7 @@ public async Task Application_name_env_var() public async Task Application_name_connection_param() { const string testAppName = "MyTestApp2"; - + await using var dataSource = CreateDataSource(csb => csb.ApplicationName = testAppName); await using var conn = await dataSource.OpenConnectionAsync(); Assert.That(conn.PostgresParameters["application_name"], Is.EqualTo(testAppName)); @@ -456,7 +456,7 @@ public async Task Application_name_connection_param_overrides_env_var() { const string envAppName = "EnvApp"; const string connAppName = "ConnApp"; - + using var _ = SetEnvironmentVariable("PGAPPNAME", envAppName); await using var dataSource = CreateDataSource(csb => csb.ApplicationName = connAppName); await using var conn = await dataSource.OpenConnectionAsync(); @@ -774,25 +774,26 @@ public async Task Set_Schemas_And_Load_Relevant_Types(string testSchema, string }); }); using var conn = await dataSource.OpenConnectionAsync(); + var databaseInfo = dataSource.CurrentReloadableState.DatabaseInfo; if (enabled) { - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); if (testSchema == "public" || otherSchema == "public") { - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); } else { - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3"), Is.False); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3"), Is.False); } } else { - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); - Assert.That(dataSource.DatabaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_1")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_2")); + Assert.That(databaseInfo.CompositeTypes.Any(x => x.Name == "test_type_3")); } } finally diff --git a/test/Npgsql.Tests/NpgsqlParameterTests.cs b/test/Npgsql.Tests/NpgsqlParameterTests.cs index cab1047015..e02031ea9f 100644 --- a/test/Npgsql.Tests/NpgsqlParameterTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterTests.cs @@ -698,7 +698,7 @@ public void Null_value_with_nullable_type() public void DBNull_reuses_type_info([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = "value" } : new NpgsqlParameter { Value = "value" }; - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); @@ -708,7 +708,7 @@ public void DBNull_reuses_type_info([Values]bool generic) Assert.That(secondTypeInfo, Is.SameAs(typeInfo)); // Make sure we don't resolve a different type info either. - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.SameAs(secondTypeInfo)); } @@ -717,7 +717,7 @@ public void DBNull_reuses_type_info([Values]bool generic) public void DBNull_followed_by_non_null_reresolves([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = DBNull.Value } : new NpgsqlParameter { Value = DBNull.Value }; - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var typeInfo, out _, out var pgTypeId); Assert.That(typeInfo, Is.Not.Null); Assert.That(pgTypeId.IsUnspecified, Is.True); @@ -727,7 +727,7 @@ public void DBNull_followed_by_non_null_reresolves([Values]bool generic) Assert.That(secondTypeInfo, Is.Null); // Make sure we don't resolve the same type info either. - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.Not.SameAs(typeInfo)); } @@ -736,7 +736,7 @@ public void DBNull_followed_by_non_null_reresolves([Values]bool generic) public void Changing_value_type_reresolves([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = "value" } : new NpgsqlParameter { Value = "value" }; - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); @@ -745,7 +745,7 @@ public void Changing_value_type_reresolves([Values]bool generic) Assert.That(secondTypeInfo, Is.Null); // Make sure we don't resolve a different type info either. - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.Not.SameAs(typeInfo)); } @@ -764,10 +764,10 @@ public void DataTypeName_prioritized_over_NpgsqlDbType([Values]bool generic) DataTypeName = "text", Value = "value" }; - param.ResolveTypeInfo(DataSource.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); - Assert.That(typeInfo.PgTypeId, Is.EqualTo(DataSource.SerializerOptions.TextPgTypeId)); + Assert.That(typeInfo.PgTypeId, Is.EqualTo(DataSource.CurrentReloadableState.SerializerOptions.TextPgTypeId)); } #if NeedsPorting diff --git a/test/Npgsql.Tests/PostgresTypeTests.cs b/test/Npgsql.Tests/PostgresTypeTests.cs index 056830cf32..7c3945fb0a 100644 --- a/test/Npgsql.Tests/PostgresTypeTests.cs +++ b/test/Npgsql.Tests/PostgresTypeTests.cs @@ -69,6 +69,6 @@ public async Task Multirange() async Task GetDatabaseInfo() { await using var conn = await OpenConnectionAsync(); - return conn.NpgsqlDataSource.DatabaseInfo; + return conn.NpgsqlDataSource.CurrentReloadableState.DatabaseInfo; } } From 0ec21969809aa9ae9da313921e4716f619a06c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20Kj=C3=B8nigsen?= Date: Sun, 16 Nov 2025 10:50:35 +0100 Subject: [PATCH 128/155] Upgrade from .NET10-RC2 to .NET10-RTM. (#6311) --- Directory.Build.props | 2 +- Directory.Packages.props | 12 ++++++------ global.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 56ff636631..a7f8f89bff 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.0-rc.2 + 10.0.0-rtm latest true enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 6864076aef..5a72db7721 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,11 +1,11 @@ - - + + - + @@ -24,15 +24,15 @@ - - + + - + diff --git a/global.json b/global.json index a08ab85427..6a288505a1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100-rc.2.25502.107", + "version": "10.0.100", "rollForward": "latestMajor", "allowPrerelease": false } From 99169989714bf16880958e06c506c68e0401e92a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:11:15 +0000 Subject: [PATCH 129/155] Bump Microsoft.Data.SqlClient from 6.1.2 to 6.1.3 (#6319) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a72db7721..fb2dc723fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,7 @@ - + From 712e0f353fb5503138289faac90860933eff96a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:19:36 +0100 Subject: [PATCH 130/155] Bump Microsoft.NET.Test.Sdk from 18.0.0 to 18.0.1 (#6320) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fb2dc723fb..46e8962ffa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From bddee32c2634b32f9d3e7c8c4cf1220782310bd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:21:18 +0000 Subject: [PATCH 131/155] Bump OpenTelemetry.Api from 1.13.1 to 1.14.0 (#6321) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 46e8962ffa..2ef94f40a2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + From 63152950a75f0f75adc3c74ce195aa3b91bd830e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:23:03 +0000 Subject: [PATCH 132/155] Bump Scriban.Signed from 6.5.0 to 6.5.1 (#6322) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2ef94f40a2..f8a9e16cd0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + From fe0f5eedb2cb72c06028b23e4319db7cc5ab2c79 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Tue, 18 Nov 2025 18:13:16 +0100 Subject: [PATCH 133/155] Add experimental to AddTypeInfoResolverFactory (#6325) --- src/Npgsql/NpgsqlDataSourceBuilder.cs | 1 + src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 1 + src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Npgsql/NpgsqlDataSourceBuilder.cs b/src/Npgsql/NpgsqlDataSourceBuilder.cs index b08f175533..b18ce75848 100644 --- a/src/Npgsql/NpgsqlDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlDataSourceBuilder.cs @@ -415,6 +415,7 @@ public NpgsqlDataSourceBuilder UseNegotiateOptionsCallback(Action + [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) => _internalBuilder.AddTypeInfoResolverFactory(factory); diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 8bfe449c4a..9f91ef66b2 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -532,6 +532,7 @@ public bool UnmapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMember => _userTypeMapper.UnmapComposite(clrType, pgName, nameTranslator); /// + [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) => _resolverChainBuilder.PrependResolverFactory(factory); /// diff --git a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs index b5f2de7594..cbb6ac8ccc 100644 --- a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs +++ b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs @@ -196,6 +196,7 @@ bool UnmapComposite( /// Typically used by plugins. /// /// The type resolver factory to be added. + [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory); /// From b16f7229270846d68044f406aaa2c405951c1d7a Mon Sep 17 00:00:00 2001 From: Paulo Mattos Date: Tue, 18 Nov 2025 16:08:43 -0300 Subject: [PATCH 134/155] Add IDbTypeResolver to allow plugins to control how DbTypes are mapped (#6267) --- src/Npgsql/Internal/ChainDbTypeResolver.cs | 33 ++++ src/Npgsql/Internal/DbTypeResolverFactory.cs | 9 + src/Npgsql/Internal/IDbTypeResolver.cs | 28 +++ src/Npgsql/Internal/NpgsqlConnector.cs | 1 + .../PgTypeInfoResolverChainBuilder.cs | 1 + src/Npgsql/Internal/Postgres/DataTypeName.cs | 24 ++- src/Npgsql/Internal/Postgres/DataTypeNames.cs | 22 +++ src/Npgsql/Npgsql.csproj | 1 + src/Npgsql/NpgsqlBinaryImporter.cs | 2 +- src/Npgsql/NpgsqlCommandBuilder.cs | 6 +- src/Npgsql/NpgsqlDataSource.cs | 17 +- src/Npgsql/NpgsqlDataSourceBuilder.cs | 4 + src/Npgsql/NpgsqlDataSourceConfiguration.cs | 2 + src/Npgsql/NpgsqlDiagnostics.cs | 1 + src/Npgsql/NpgsqlParameter.cs | 181 ++++++++++++------ src/Npgsql/NpgsqlParameterCollection.cs | 2 +- src/Npgsql/NpgsqlParameter`.cs | 1 + src/Npgsql/NpgsqlSlimDataSourceBuilder.cs | 9 +- src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs | 12 +- src/Npgsql/PublicAPI.Unshipped.txt | 1 + src/Npgsql/TypeMapping/GlobalTypeMapper.cs | 5 +- src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs | 8 + test/Npgsql.Tests/CommandParameterTests.cs | 2 +- test/Npgsql.Tests/DataTypeNameTests.cs | 3 + test/Npgsql.Tests/Npgsql.Tests.csproj | 1 + test/Npgsql.Tests/NpgsqlParameterTests.cs | 16 +- test/Npgsql.Tests/Support/TestBase.cs | 62 +++--- test/Npgsql.Tests/TypeMapperTests.cs | 168 ++++++++++++++++ .../Types/DateTimeInfinityTests.cs | 6 +- test/Npgsql.Tests/Types/MiscTypeTests.cs | 9 - 30 files changed, 513 insertions(+), 124 deletions(-) create mode 100644 src/Npgsql/Internal/ChainDbTypeResolver.cs create mode 100644 src/Npgsql/Internal/DbTypeResolverFactory.cs create mode 100644 src/Npgsql/Internal/IDbTypeResolver.cs diff --git a/src/Npgsql/Internal/ChainDbTypeResolver.cs b/src/Npgsql/Internal/ChainDbTypeResolver.cs new file mode 100644 index 0000000000..16f3c229ee --- /dev/null +++ b/src/Npgsql/Internal/ChainDbTypeResolver.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Npgsql.Internal.Postgres; + +namespace Npgsql.Internal; + +sealed class ChainDbTypeResolver(IEnumerable resolvers) : IDbTypeResolver +{ + readonly IDbTypeResolver[] _resolvers = new List(resolvers).ToArray(); + + public string? GetDataTypeName(DbType dbType, Type? type) + { + foreach (var resolver in _resolvers) + { + if (resolver.GetDataTypeName(dbType, type) is { } dataTypeName) + return dataTypeName; + } + + return null; + } + + public DbType? GetDbType(DataTypeName dataTypeName) + { + foreach (var resolver in _resolvers) + { + if (resolver.GetDbType(dataTypeName) is { } dbType) + return dbType; + } + + return null; + } +} diff --git a/src/Npgsql/Internal/DbTypeResolverFactory.cs b/src/Npgsql/Internal/DbTypeResolverFactory.cs new file mode 100644 index 0000000000..55b3b71235 --- /dev/null +++ b/src/Npgsql/Internal/DbTypeResolverFactory.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Npgsql.Internal; + +[Experimental(NpgsqlDiagnostics.DbTypeResolverExperimental)] +public abstract class DbTypeResolverFactory +{ + public abstract IDbTypeResolver CreateDbTypeResolver(NpgsqlDatabaseInfo databaseInfo); +} diff --git a/src/Npgsql/Internal/IDbTypeResolver.cs b/src/Npgsql/Internal/IDbTypeResolver.cs new file mode 100644 index 0000000000..c4586a2bee --- /dev/null +++ b/src/Npgsql/Internal/IDbTypeResolver.cs @@ -0,0 +1,28 @@ +using System; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using Npgsql.Internal.Postgres; + +namespace Npgsql.Internal; + +/// +/// An Npgsql resolver for DbType. Used by Npgsql to resolve a DbType to DataTypeName and back. +/// +[Experimental(NpgsqlDiagnostics.DbTypeResolverExperimental)] +public interface IDbTypeResolver +{ + /// + /// Attempts to resolve a DbType to a data type name. + /// + /// The DbType name to resolve. + /// The type of the value to resolve a data type name for. + /// The data type name if it could be mapped, the name can be non-normalized and without schema. + string? GetDataTypeName(DbType dbType, Type? type); + + /// + /// Attempts to resolve a data type name to a DbType. + /// + /// The data type name to map, in a normalized form but possibly without schema. + /// The DbType if it could be mapped, null otherwise. + DbType? GetDbType(DataTypeName dataTypeName); +} diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index bdd82c761f..bf6f97c1d9 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -124,6 +124,7 @@ internal string InferredUserName /// public NpgsqlDatabaseInfo DatabaseInfo => ReloadableState.DatabaseInfo; internal PgSerializerOptions SerializerOptions => ReloadableState.SerializerOptions; + internal IDbTypeResolver? DbTypeResolver => ReloadableState.DbTypeResolver; /// /// The current transaction status for this connector. diff --git a/src/Npgsql/Internal/PgTypeInfoResolverChainBuilder.cs b/src/Npgsql/Internal/PgTypeInfoResolverChainBuilder.cs index f83fa384f4..86c96231a0 100644 --- a/src/Npgsql/Internal/PgTypeInfoResolverChainBuilder.cs +++ b/src/Npgsql/Internal/PgTypeInfoResolverChainBuilder.cs @@ -115,6 +115,7 @@ public PgTypeInfoResolverChain Build(Action>? configur _addRangeResolvers?.Invoke(instance, resolvers); _addMultirangeResolvers?.Invoke(instance, resolvers); _addArrayResolvers?.Invoke(instance, resolvers); + configure?.Invoke(resolvers); return new( resolvers, diff --git a/src/Npgsql/Internal/Postgres/DataTypeName.cs b/src/Npgsql/Internal/Postgres/DataTypeName.cs index e1b8225911..9c9f43e41a 100644 --- a/src/Npgsql/Internal/Postgres/DataTypeName.cs +++ b/src/Npgsql/Internal/Postgres/DataTypeName.cs @@ -5,12 +5,14 @@ namespace Npgsql.Internal.Postgres; /// -/// Represents the fully-qualified name of a PostgreSQL type. +/// Represents the normalized name of a PostgreSQL data type. /// [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] [DebuggerDisplay("{DisplayName,nq}")] public readonly struct DataTypeName : IEquatable { + const char InvalidIdentifier = '-'; + /// /// The maximum length of names in an unmodified PostgreSQL installation. /// @@ -50,9 +52,9 @@ public DataTypeName(string fullyQualifiedDataTypeName) internal static DataTypeName ValidatedName(string fullyQualifiedDataTypeName) => new(fullyQualifiedDataTypeName, validated: true); - // Includes schema unless it's pg_catalog or the name is unspecified. + // 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) || Value == Unspecified + Value.StartsWith("pg_catalog", StringComparison.Ordinal) || IsUnqualified ? UnqualifiedDisplayName : Schema + "." + UnqualifiedDisplayName; @@ -71,12 +73,19 @@ static string ThrowDefaultException() => // This contains two invalid sql identifiers (schema and name are both separate identifiers, and would both have to be quoted to be valid). // Given this is an invalid name it's fine for us to represent a fully qualified 'unspecified' name with it. - public static DataTypeName Unspecified => new("-.-", validated: true); + static string UnspecifiedName => $"{InvalidIdentifier}.{InvalidIdentifier}"; + public static DataTypeName Unspecified => ValidatedName(UnspecifiedName); + + public static string GetUnqualifiedName(string dataTypeName) + => dataTypeName.IndexOf('.') is not -1 and var index + ? dataTypeName.Substring(index + 1) : dataTypeName; + + public bool IsUnqualified => Value.StartsWith(InvalidIdentifier) && Value != UnspecifiedName; public bool IsArray => UnqualifiedNameSpan.StartsWith("_".AsSpan(), StringComparison.Ordinal); internal static DataTypeName CreateFullyQualifiedName(string dataTypeName) - => dataTypeName.IndexOf('.') != -1 ? new(dataTypeName) : new("pg_catalog." + dataTypeName); + => dataTypeName.IndexOf('.') != -1 ? new(dataTypeName) : new("-." + dataTypeName); // Static transform as defined by https://www.postgresql.org/docs/current/sql-createtype.html#SQL-CREATETYPE-ARRAY // We don't have to deal with [] as we're always starting from a normalized fully qualified name. @@ -135,7 +144,7 @@ internal static DataTypeName FromDisplayName(string displayName, string? schema, } else { - schemaSpan = schema is null ? "pg_catalog" : schema.AsSpan(); + schemaSpan = schema is null ? $"{InvalidIdentifier}" : schema.AsSpan(); } // Then we strip either of the two valid array representations to get the base type name (with or without facets). @@ -187,6 +196,9 @@ internal static DataTypeName FromDisplayName(string displayName, string? schema, var value => value }; + if (schema is null && DataTypeNames.IsWellKnownUnqualifiedName(mapped)) + schemaSpan = "pg_catalog".AsSpan(); + return new(string.Concat(schemaSpan, ".", isArray ? "_" : "", mapped)); } diff --git a/src/Npgsql/Internal/Postgres/DataTypeNames.cs b/src/Npgsql/Internal/Postgres/DataTypeNames.cs index 275bcb9937..6c4ca73b2f 100644 --- a/src/Npgsql/Internal/Postgres/DataTypeNames.cs +++ b/src/Npgsql/Internal/Postgres/DataTypeNames.cs @@ -1,3 +1,4 @@ +using System; using static Npgsql.Internal.Postgres.DataTypeName; namespace Npgsql.Internal.Postgres; @@ -7,6 +8,27 @@ namespace Npgsql.Internal.Postgres; /// static class DataTypeNames { + // Generated from the following query: + // SELECT '"' || string_agg(typname, '" or "') || '"' FROM ( + // SELECT typname FROM pg_catalog.pg_type WHERE typtype = 'b' AND typcategory <> 'A' + // AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'pg_catalog') ORDER BY typname); + public static bool IsWellKnownUnqualifiedName(ReadOnlySpan name) => name switch + { + "aclitem" or "bit" or "bool" or "box" or "bpchar" or "bytea" or "char" or "cid" or + "cidr" or "circle" or "date" or "float4" or "float8" or "gtsvector" or "inet" or + "int2" or "int4" or "int8" or "interval" or "json" or "jsonb" or "jsonpath" or + "line" or "lseg" or "macaddr" or "macaddr8" or "money" or "name" or "numeric" or + "oid" or "path" or "pg_brin_bloom_summary" or "pg_brin_minmax_multi_summary" or + "pg_dependencies" or "pg_lsn" or "pg_mcv_list" or "pg_ndistinct" or "pg_node_tree" or + "pg_snapshot" or "point" or "polygon" or "refcursor" or "regclass" or "regcollation" or + "regconfig" or "regdictionary" or "regnamespace" or "regoper" or "regoperator" or + "regproc" or "regprocedure" or "regrole" or "regtype" or "text" or "tid" or "time" or + "timestamp" or "timestamptz" or "timetz" or "tsquery" or "tsvector" or "txid_snapshot" or + "uuid" or "varbit" or "varchar" or "xid" or "xid8" or "xml" + => true, + _ => false + }; + // Note: The names are fully qualified in source so the strings are constants and instances will be interned after the first call. // Uses an internal constructor bypassing the public DataTypeName constructor validation, as we don't want to store all these names on // fields either. diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 5273736ceb..0eab75cd66 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -9,6 +9,7 @@ $(NoWarn);CA2017 $(NoWarn);NPG9001 $(NoWarn);NPG9002 + $(NoWarn);NPG9003 diff --git a/src/Npgsql/NpgsqlBinaryImporter.cs b/src/Npgsql/NpgsqlBinaryImporter.cs index 52c9438fde..8c240468d4 100644 --- a/src/Npgsql/NpgsqlBinaryImporter.cs +++ b/src/Npgsql/NpgsqlBinaryImporter.cs @@ -282,7 +282,7 @@ async Task Core(bool async, T value, NpgsqlDbType? npgsqlDbType, string? dataTyp // These actions can reset or change the type info, we'll check afterwards whether we're still consistent with the original values. param.TypedValue = value; - param.ResolveTypeInfo(_connector.SerializerOptions); + param.ResolveTypeInfo(_connector.SerializerOptions, _connector.DbTypeResolver); if (previousTypeInfo is not null && previousConverter is not null && param.PgTypeId != previousTypeId) { diff --git a/src/Npgsql/NpgsqlCommandBuilder.cs b/src/Npgsql/NpgsqlCommandBuilder.cs index 9665b8356c..d9a698c2ef 100644 --- a/src/Npgsql/NpgsqlCommandBuilder.cs +++ b/src/Npgsql/NpgsqlCommandBuilder.cs @@ -212,7 +212,11 @@ private static void SetParameterValuesFromRow(NpgsqlCommand command, DataRow row protected override void ApplyParameterInfo(DbParameter p, DataRow row, System.Data.StatementType statementType, bool whereClause) { var param = (NpgsqlParameter)p; - param.NpgsqlDbType = (NpgsqlDbType)row[SchemaTableColumn.ProviderType]; + // DbCommandBuilder is going to set DbType.Int32 onto an existing parameter, reset other db type fields. + if (param.SourceColumnNullMapping) + param.ResetDbType(); + else + param.NpgsqlDbType = (NpgsqlDbType)row[SchemaTableColumn.ProviderType]; } /// diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 51042bb654..000d3f1ae3 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -31,11 +31,12 @@ public abstract class NpgsqlDataSource : DbDataSource internal NpgsqlLoggingConfiguration LoggingConfiguration { get; } readonly PgTypeInfoResolverChain _resolverChain; + readonly IEnumerable _dbTypeResolverFactories; internal ReloadableState CurrentReloadableState = null!; // Initialized during bootstrapping. // Initialized at bootstrapping - internal sealed class ReloadableState(NpgsqlDatabaseInfo databaseInfo, PgSerializerOptions serializerOptions) + internal sealed class ReloadableState(NpgsqlDatabaseInfo databaseInfo, PgSerializerOptions serializerOptions, IDbTypeResolver? dbTypeResolver) { /// /// Information about PostgreSQL and PostgreSQL-like databases (e.g. type definitions, capabilities...). @@ -43,8 +44,11 @@ internal sealed class ReloadableState(NpgsqlDatabaseInfo databaseInfo, PgSeriali public NpgsqlDatabaseInfo DatabaseInfo { get; } = databaseInfo; public PgSerializerOptions SerializerOptions { get; } = serializerOptions; + + public IDbTypeResolver? DbTypeResolver { get; } = dbTypeResolver; } + internal TransportSecurityHandler TransportSecurityHandler { get; } internal Action? SslClientAuthenticationOptionsCallback { get; } @@ -113,6 +117,7 @@ internal NpgsqlDataSource( _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval, _resolverChain, + _dbTypeResolverFactories, _defaultNameTranslator, ConnectionInitializer, ConnectionInitializerAsync, @@ -284,7 +289,8 @@ internal async Task Bootstrap( { TextEncoding = connector.TextEncoding, TypeInfoResolver = AdoTypeInfoResolverFactory.Instance.CreateResolver(), - }); + }, + dbTypeResolver: null); NpgsqlDatabaseInfo databaseInfo; @@ -299,9 +305,14 @@ internal async Task Bootstrap( DefaultNameTranslator = _defaultNameTranslator }; + var resolvers = new List(); + foreach (var dbTypeResolverFactory in _dbTypeResolverFactories) + resolvers.Add(dbTypeResolverFactory.CreateDbTypeResolver(databaseInfo)); + connector.ReloadableState = CurrentReloadableState = new ReloadableState( databaseInfo: databaseInfo, - serializerOptions: serializerOptions); + serializerOptions: serializerOptions, + dbTypeResolver: new ChainDbTypeResolver(resolvers)); IsBootstrapped = true; } diff --git a/src/Npgsql/NpgsqlDataSourceBuilder.cs b/src/Npgsql/NpgsqlDataSourceBuilder.cs index b18ce75848..bc4b65d7f6 100644 --- a/src/Npgsql/NpgsqlDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlDataSourceBuilder.cs @@ -414,6 +414,10 @@ public NpgsqlDataSourceBuilder UseNegotiateOptionsCallback(Action + void INpgsqlTypeMapper.AddDbTypeResolverFactory(DbTypeResolverFactory factory) + => ((INpgsqlTypeMapper)_internalBuilder).AddDbTypeResolverFactory(factory); + /// [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) diff --git a/src/Npgsql/NpgsqlDataSourceConfiguration.cs b/src/Npgsql/NpgsqlDataSourceConfiguration.cs index 8ba336cd5b..f3cdd4b513 100644 --- a/src/Npgsql/NpgsqlDataSourceConfiguration.cs +++ b/src/Npgsql/NpgsqlDataSourceConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Security; using System.Threading; using System.Threading.Tasks; @@ -19,6 +20,7 @@ sealed record NpgsqlDataSourceConfiguration(string? Name, TimeSpan PeriodicPasswordSuccessRefreshInterval, TimeSpan PeriodicPasswordFailureRefreshInterval, PgTypeInfoResolverChain ResolverChain, + IEnumerable DbTypeResolverFactories, INpgsqlNameTranslator DefaultNameTranslator, Action? ConnectionInitializer, Func? ConnectionInitializerAsync, diff --git a/src/Npgsql/NpgsqlDiagnostics.cs b/src/Npgsql/NpgsqlDiagnostics.cs index 2037fec667..0d9ff5f846 100644 --- a/src/Npgsql/NpgsqlDiagnostics.cs +++ b/src/Npgsql/NpgsqlDiagnostics.cs @@ -4,4 +4,5 @@ static class NpgsqlDiagnostics { public const string ConvertersExperimental = "NPG9001"; public const string DatabaseInfoExperimental = "NPG9002"; + public const string DbTypeResolverExperimental = "NPG9003"; } diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index ca7ec17cbb..8930724c92 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -30,6 +30,7 @@ public class NpgsqlParameter : DbParameter, IDbDataParameter, ICloneable internal NpgsqlDbType? _npgsqlDbType; internal string? _dataTypeName; + internal DbType? _dbType; private protected string _name = string.Empty; object? _value; @@ -40,6 +41,7 @@ public class NpgsqlParameter : DbParameter, IDbDataParameter, ICloneable internal string TrimmedName { get; private protected set; } = PositionalName; internal const string PositionalName = ""; + IDbTypeResolver? _dbTypeResolver; private protected PgTypeInfo? TypeInfo { get; private set; } internal PgTypeId PgTypeId { get; private set; } @@ -315,26 +317,32 @@ public sealed override DbType DbType { get { - if (_npgsqlDbType is { } npgsqlDbType) - return npgsqlDbType.ToDbType(); + if (_dbType is { } dbType) + return dbType; if (_dataTypeName is not null) - return Internal.Postgres.DataTypeName.FromDisplayName(_dataTypeName).ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; + { + var dataTypeName = Internal.Postgres.DataTypeName.FromDisplayName(_dataTypeName); + if (TryResolveDbType(dataTypeName, out var resolvedDbType)) + return resolvedDbType; + + return dataTypeName.ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; + } + + if (_npgsqlDbType is { } npgsqlDbType) + return npgsqlDbType.ToDbType(); // Infer from value but don't cache - if (Value is not null) - // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. - return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; + // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. + if (GetValueType(StaticValueType) is { } valueType) + return GlobalTypeMapper.Instance.FindDataTypeName(valueType, Value)?.ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; return DbType.Object; } set { ResetTypeInfo(); - _npgsqlDbType = value == DbType.Object - ? null - : value.ToNpgsqlDbType() - ?? throw new NotSupportedException($"The parameter type DbType.{value} isn't supported by PostgreSQL or Npgsql"); + _dbType = value; } } @@ -355,10 +363,19 @@ public NpgsqlDbType NpgsqlDbType if (_dataTypeName is not null) return Internal.Postgres.DataTypeName.FromDisplayName(_dataTypeName).ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; + var valueType = GetValueType(StaticValueType); + if (_dbType is { } dbType) + { + if (TryResolveDbTypeDataTypeName(dbType, valueType, out var dataTypeName)) + return NpgsqlDbTypeExtensions.ToNpgsqlDbType(dataTypeName) ?? NpgsqlDbType.Unknown; + + return dbType.ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; + } + // Infer from value but don't cache - if (Value is not null) - // We pass ValueType here for the generic derived type (NpgsqlParameter) where we should respect T and not the runtime type. - return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; + // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. + if (valueType is not null) + return GlobalTypeMapper.Instance.FindDataTypeName(valueType, Value)?.ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; return NpgsqlDbType.Unknown; } @@ -392,10 +409,21 @@ public string? DataTypeName "pg_catalog." + unqualifiedName).UnqualifiedDisplayName; } + var valueType = GetValueType(StaticValueType); + if (_dbType is { } dbType) + { + if (TryResolveDbTypeDataTypeName(dbType, valueType, out var dataTypeName)) + return dataTypeName; + + var unqualifiedName = dbType.ToNpgsqlDbType()?.ToUnqualifiedDataTypeName(); + return unqualifiedName is null ? null : Internal.Postgres.DataTypeName.ValidatedName( + "pg_catalog." + unqualifiedName).UnqualifiedDisplayName; + } + // Infer from value but don't cache - if (Value is not null) - // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. - return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.DisplayName; + // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. + if (valueType is not null) + return GlobalTypeMapper.Instance.FindDataTypeName(valueType, Value)?.DisplayName; return null; } @@ -497,6 +525,30 @@ public sealed override string SourceColumn Type? GetValueType(Type staticValueType) => staticValueType != typeof(object) ? staticValueType : Value?.GetType(); + bool TryResolveDbType(DataTypeName dataTypeName, out DbType dbType) + { + if (_dbTypeResolver?.GetDbType(dataTypeName) is { } result) + { + dbType = result; + return true; + } + + dbType = default; + return false; + } + + bool TryResolveDbTypeDataTypeName(DbType dbType, Type? type, [NotNullWhen(true)]out string? normalizedDataTypeName) + { + if (_dbTypeResolver?.GetDataTypeName(dbType, type) is { } result) + { + normalizedDataTypeName = Internal.Postgres.DataTypeName.NormalizeName(result); + return true; + } + + normalizedDataTypeName = null; + return false; + } + internal void SetOutputValue(NpgsqlDataReader reader, int ordinal) { if (GetType() == typeof(NpgsqlParameter)) @@ -536,18 +588,44 @@ internal void SetResolutionInfo(PgTypeInfo typeInfo, PgConverter converter, PgTy } /// Attempt to resolve a type info based on available (postgres) type information on the parameter. - internal void ResolveTypeInfo(PgSerializerOptions options) + internal void ResolveTypeInfo(PgSerializerOptions options, IDbTypeResolver? dbTypeResolver) { var typeInfo = TypeInfo; var previouslyResolved = ReferenceEquals(typeInfo?.Options, options); if (!previouslyResolved) { - var dataTypeName = - _dataTypeName is not null - ? Internal.Postgres.DataTypeName.NormalizeName(_dataTypeName) - : _npgsqlDbType is { } npgsqlDbType - ? npgsqlDbType.ToDataTypeName() ?? npgsqlDbType.ToUnqualifiedDataTypeNameOrThrow() - : null; + var staticValueType = StaticValueType; + var valueType = GetValueType(staticValueType); + + string? dataTypeName = null; + if (_dataTypeName is not null) + { + dataTypeName = Internal.Postgres.DataTypeName.NormalizeName(_dataTypeName); + } + else if (_npgsqlDbType is { } npgsqlDbType) + { + dataTypeName = npgsqlDbType.ToDataTypeName() ?? npgsqlDbType.ToUnqualifiedDataTypeNameOrThrow(); + } + else if (_dbType is { } dbType) + { + if (dbTypeResolver is not null) + { + _dbTypeResolver = dbTypeResolver; + if (dbTypeResolver.GetDataTypeName(dbType, valueType) is { } result) + { + dataTypeName = Internal.Postgres.DataTypeName.NormalizeName(result); + } + } + + // Fall back to builtin mappings if there was no resolver, or it didn't produce a result. + if (dataTypeName is null) + { + dataTypeName = dbType.ToNpgsqlDbType()?.ToDataTypeName(); + // If DbType.Object was specified we will only throw (see ThrowNoTypeInfo) if valueType is also null. + if (dataTypeName is null && dbType is not DbType.Object) + ThrowDbTypeNotSupported(); + } + } PgTypeId? pgTypeId = null; if (dataTypeName is not null) @@ -561,35 +639,24 @@ _dataTypeName is not null pgTypeId = options.ToCanonicalTypeId(pgType.GetRepresentationalType()); } - var unspecifiedDBNull = false; - var valueType = StaticValueType; - if (valueType == typeof(object)) + if (pgTypeId is null && valueType is null) { - valueType = Value?.GetType(); - if (valueType is null && pgTypeId is null) - { - ThrowNoTypeInfo(); - return; - } - - // We treat object typed DBNull values as default info. - // Unless we don't have a pgTypeId either, at which point we'll use an 'unspecified' PgTypeInfo to help us write a NULL. - if (valueType == typeof(DBNull)) - { - if (pgTypeId is null) - { - unspecifiedDBNull = true; - typeInfo = options.UnspecifiedDBNullTypeInfo; - } - else - valueType = null; - } + ThrowNoTypeInfo(); + return; } - if (!unspecifiedDBNull) - typeInfo = AdoSerializerHelpers.GetTypeInfoForWriting(valueType, pgTypeId, options, _npgsqlDbType); - - TypeInfo = typeInfo; + // We treat object typed DBNull values as default info (we don't supply a type). + // Unless we don't have a pgTypeId either, at which point we'll use an 'unspecified' PgTypeInfo to help us write a NULL. + if (valueType == typeof(DBNull) && staticValueType == typeof(object)) + { + TypeInfo = typeInfo = pgTypeId is null + ? options.UnspecifiedDBNullTypeInfo + : AdoSerializerHelpers.GetTypeInfoForWriting(type: null, pgTypeId, options, _npgsqlDbType); + } + else + { + TypeInfo = typeInfo = AdoSerializerHelpers.GetTypeInfoForWriting(valueType, pgTypeId, options, _npgsqlDbType); + } } // This step isn't part of BindValue because we need to know the PgTypeId beforehand for things like SchemaOnly with null values. @@ -605,14 +672,16 @@ _dataTypeName is not null void ThrowNoTypeInfo() => ThrowHelper.ThrowInvalidOperationException( - $"Parameter '{(!string.IsNullOrEmpty(ParameterName) ? ParameterName : $"${Collection?.IndexOf(this) + 1}")}' must have either its NpgsqlDbType or its DataTypeName or its Value set."); + $"Parameter '{(!string.IsNullOrEmpty(ParameterName) ? ParameterName : $"${Collection?.IndexOf(this) + 1}")}' must have either its DbType, NpgsqlDbType, DataTypeName or its Value set."); + + void ThrowDbTypeNotSupported() + => ThrowHelper.ThrowNotSupportedException( + $"The DbType '{_dbType}' isn't supported by Npgsql. There might be an Npgsql plugin with support for this DbType."); void ThrowNotSupported(string dataTypeName) - { - ThrowHelper.ThrowNotSupportedException(_npgsqlDbType is not null - ? $"The NpgsqlDbType '{_npgsqlDbType}' isn't present in your database. You may need to install an extension or upgrade to a newer version." - : $"The data type name '{dataTypeName}' isn't present in your database. You may need to install an extension or upgrade to a newer version."); - } + => ThrowHelper.ThrowNotSupportedException( + $"The data type name '{dataTypeName}'{(_npgsqlDbType is not null ? $", provided as NpgsqlDbType '{_npgsqlDbType}'," : null)} could not be found in the types that were loaded by Npgsql. " + + $"Your database details or Npgsql type loading configuration may be incorrect. Alternatively your PostgreSQL installation might need to be upgraded, or an extension adding the missing data type might not have been installed."); } // Pull from Value so we also support object typed generic params. @@ -755,6 +824,7 @@ private protected virtual ValueTask WriteValue(bool async, PgWriter writer, Canc /// public override void ResetDbType() { + _dbType = null; _npgsqlDbType = null; _dataTypeName = null; ResetTypeInfo(); @@ -815,6 +885,7 @@ private protected virtual NpgsqlParameter CloneCore() => _precision = _precision, _scale = _scale, _size = _size, + _dbType = _dbType, _npgsqlDbType = _npgsqlDbType, _dataTypeName = _dataTypeName, Direction = Direction, diff --git a/src/Npgsql/NpgsqlParameterCollection.cs b/src/Npgsql/NpgsqlParameterCollection.cs index 17e7fb7969..85b418b157 100644 --- a/src/Npgsql/NpgsqlParameterCollection.cs +++ b/src/Npgsql/NpgsqlParameterCollection.cs @@ -724,7 +724,7 @@ internal void ProcessParameters(NpgsqlDataSource.ReloadableState reloadableState break; } - p.ResolveTypeInfo(reloadableState.SerializerOptions); + p.ResolveTypeInfo(reloadableState.SerializerOptions, reloadableState.DbTypeResolver); if (validateValues) { diff --git a/src/Npgsql/NpgsqlParameter`.cs b/src/Npgsql/NpgsqlParameter`.cs index d353cdce45..2f1e1b24bc 100644 --- a/src/Npgsql/NpgsqlParameter`.cs +++ b/src/Npgsql/NpgsqlParameter`.cs @@ -138,6 +138,7 @@ private protected override NpgsqlParameter CloneCore() => _precision = _precision, _scale = _scale, _size = _size, + _dbType = _dbType, _npgsqlDbType = _npgsqlDbType, _dataTypeName = _dataTypeName, Direction = Direction, diff --git a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs index 9f91ef66b2..e8d8ea5061 100644 --- a/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs +++ b/src/Npgsql/NpgsqlSlimDataSourceBuilder.cs @@ -48,6 +48,7 @@ public sealed class NpgsqlSlimDataSourceBuilder : INpgsqlTypeMapper Func>? _periodicPasswordProvider; TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval; + List? _dbTypeResolverFactories; PgTypeInfoResolverChainBuilder _resolverChainBuilder = new(); // mutable struct, don't make readonly. readonly UserTypeMapper _userTypeMapper; @@ -531,9 +532,14 @@ public bool UnmapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMember Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) => _userTypeMapper.UnmapComposite(clrType, pgName, nameTranslator); + /// + void INpgsqlTypeMapper.AddDbTypeResolverFactory(DbTypeResolverFactory factory) + => (_dbTypeResolverFactories ??= new()).Add(factory); + /// [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] - public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) => _resolverChainBuilder.PrependResolverFactory(factory); + public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) + => _resolverChainBuilder.PrependResolverFactory(factory); /// void INpgsqlTypeMapper.Reset() => _resolverChainBuilder.Clear(); @@ -888,6 +894,7 @@ _loggerFactory is null _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval, _resolverChainBuilder.Build(ConfigureResolverChain), + _dbTypeResolverFactories ?? [], DefaultNameTranslator, _connectionInitializer, _connectionInitializerAsync, diff --git a/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs b/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs index abb24c74d0..f9b952e479 100644 --- a/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs +++ b/src/Npgsql/NpgsqlTypes/NpgsqlDbType.cs @@ -876,11 +876,11 @@ _ when npgsqlDbType.HasFlag(NpgsqlDbType.Multirange) internal static NpgsqlDbType? ToNpgsqlDbType(this DataTypeName dataTypeName) => ToNpgsqlDbType(dataTypeName.UnqualifiedName); /// Should not be used with display names, first normalize it instead. - internal static NpgsqlDbType? ToNpgsqlDbType(string dataTypeName) + internal static NpgsqlDbType? ToNpgsqlDbType(string normalizedDataTypeName) { - var unqualifiedName = dataTypeName; - if (dataTypeName.IndexOf(".", StringComparison.Ordinal) is not -1 and var index) - unqualifiedName = dataTypeName.Substring(0, index); + var unqualifiedName = normalizedDataTypeName.AsSpan(); + if (unqualifiedName.IndexOf('.') is not -1 and var index) + unqualifiedName = unqualifiedName.Slice(index + 1); return unqualifiedName switch { @@ -979,12 +979,12 @@ _ when npgsqlDbType.HasFlag(NpgsqlDbType.Multirange) "geometry" => NpgsqlDbType.Geometry, "geography" => NpgsqlDbType.Geography, - _ when unqualifiedName.Contains("unknown") + _ when unqualifiedName.IndexOf("unknown") != -1 => !unqualifiedName.StartsWith("_", StringComparison.Ordinal) ? NpgsqlDbType.Unknown : null, _ when unqualifiedName.StartsWith("_", StringComparison.Ordinal) - => ToNpgsqlDbType(unqualifiedName.Substring(1)) is { } elementNpgsqlDbType + => ToNpgsqlDbType(unqualifiedName.Slice(1).ToString()) is { } elementNpgsqlDbType ? elementNpgsqlDbType | NpgsqlDbType.Array : null, // e.g. custom ranges, plugin types etc. diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index bb06704140..52f47e43d7 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -4,6 +4,7 @@ Npgsql.GssEncryptionMode Npgsql.GssEncryptionMode.Disable = 0 -> Npgsql.GssEncryptionMode Npgsql.GssEncryptionMode.Prefer = 1 -> Npgsql.GssEncryptionMode Npgsql.GssEncryptionMode.Require = 2 -> Npgsql.GssEncryptionMode +Npgsql.TypeMapping.INpgsqlTypeMapper.AddDbTypeResolverFactory(Npgsql.Internal.DbTypeResolverFactory! factory) -> void Npgsql.NpgsqlConnection.CloneWithAsync(string! connectionString, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Action? Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs index 2f9b5b3479..ef3981d22f 100644 --- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs +++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs @@ -95,7 +95,7 @@ PgSerializerOptions TypeMappingOptions } } - internal DataTypeName? FindDataTypeName(Type type, object value) + internal DataTypeName? FindDataTypeName(Type type, object? value) { DataTypeName? dataTypeName; try @@ -148,6 +148,9 @@ public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory) } } + 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(); diff --git a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs index cbb6ac8ccc..3fc5d0cbf1 100644 --- a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs +++ b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs @@ -199,6 +199,14 @@ bool UnmapComposite( [Experimental(NpgsqlDiagnostics.ConvertersExperimental)] void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory); + /// + /// Adds a DbType resolver factory which can change how DbType cases are mapped to PostgreSQL data types. + /// Typically used by plugins. + /// + /// The resolver factory to be added. + [Experimental(NpgsqlDiagnostics.DbTypeResolverExperimental)] + public void AddDbTypeResolverFactory(DbTypeResolverFactory factory); + /// /// Configures the JSON serializer options used when reading and writing all System.Text.Json data. /// diff --git a/test/Npgsql.Tests/CommandParameterTests.cs b/test/Npgsql.Tests/CommandParameterTests.cs index 3e758f2413..7f5a5dc5c4 100644 --- a/test/Npgsql.Tests/CommandParameterTests.cs +++ b/test/Npgsql.Tests/CommandParameterTests.cs @@ -189,7 +189,7 @@ public async Task Parameter_must_be_set(bool genericParam) Assert.That(async () => await cmd.ExecuteReaderAsync(), Throws.Exception .TypeOf() - .With.Message.EqualTo("Parameter 'p1' must have either its NpgsqlDbType or its DataTypeName or its Value set.")); + .With.Message.EqualTo("Parameter 'p1' must have either its DbType, NpgsqlDbType, DataTypeName or its Value set.")); } [Test] diff --git a/test/Npgsql.Tests/DataTypeNameTests.cs b/test/Npgsql.Tests/DataTypeNameTests.cs index 5c64baa607..067eb217c4 100644 --- a/test/Npgsql.Tests/DataTypeNameTests.cs +++ b/test/Npgsql.Tests/DataTypeNameTests.cs @@ -51,6 +51,9 @@ public string ToDefaultMultirangeNameHasRange(string name) [TestCase("name", null, ExpectedResult = "pg_catalog.name")] [TestCase("_name", null, ExpectedResult = "pg_catalog._name")] [TestCase("name[]", null, ExpectedResult = "pg_catalog._name")] + [TestCase("mytype", null, ExpectedResult = "-.mytype")] + [TestCase("_mytype", null, ExpectedResult = "-._mytype")] + [TestCase("mytype[]", null, ExpectedResult = "-._mytype")] [TestCase("character varying", null, ExpectedResult = "pg_catalog.varchar")] [TestCase("decimal(facet_name)", null, ExpectedResult = "pg_catalog.numeric")] [TestCase("name", "public", ExpectedResult = "public.name")] diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index d2da055a6e..2944755e6c 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -22,5 +22,6 @@ true $(NoWarn);NPG9001 $(NoWarn);NPG9002 + $(NoWarn);NPG9003 diff --git a/test/Npgsql.Tests/NpgsqlParameterTests.cs b/test/Npgsql.Tests/NpgsqlParameterTests.cs index e02031ea9f..e1f0ef9c48 100644 --- a/test/Npgsql.Tests/NpgsqlParameterTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterTests.cs @@ -133,7 +133,7 @@ public void Setting_NpgsqlDbType_sets_DbType() [Test] public void Setting_value_does_not_change_DbType() { - var p = new NpgsqlParameter { DbType = DbType.String, NpgsqlDbType = NpgsqlDbType.Bytea }; + var p = new NpgsqlParameter { DbType = DbType.Binary, NpgsqlDbType = NpgsqlDbType.Bytea }; p.Value = 8; Assert.That(p.DbType, Is.EqualTo(DbType.Binary)); Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea)); @@ -698,7 +698,7 @@ public void Null_value_with_nullable_type() public void DBNull_reuses_type_info([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = "value" } : new NpgsqlParameter { Value = "value" }; - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); @@ -708,7 +708,7 @@ public void DBNull_reuses_type_info([Values]bool generic) Assert.That(secondTypeInfo, Is.SameAs(typeInfo)); // Make sure we don't resolve a different type info either. - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.SameAs(secondTypeInfo)); } @@ -717,7 +717,7 @@ public void DBNull_reuses_type_info([Values]bool generic) public void DBNull_followed_by_non_null_reresolves([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = DBNull.Value } : new NpgsqlParameter { Value = DBNull.Value }; - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var typeInfo, out _, out var pgTypeId); Assert.That(typeInfo, Is.Not.Null); Assert.That(pgTypeId.IsUnspecified, Is.True); @@ -727,7 +727,7 @@ public void DBNull_followed_by_non_null_reresolves([Values]bool generic) Assert.That(secondTypeInfo, Is.Null); // Make sure we don't resolve the same type info either. - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.Not.SameAs(typeInfo)); } @@ -736,7 +736,7 @@ public void DBNull_followed_by_non_null_reresolves([Values]bool generic) public void Changing_value_type_reresolves([Values]bool generic) { var param = generic ? new NpgsqlParameter { Value = "value" } : new NpgsqlParameter { Value = "value" }; - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); @@ -745,7 +745,7 @@ public void Changing_value_type_reresolves([Values]bool generic) Assert.That(secondTypeInfo, Is.Null); // Make sure we don't resolve a different type info either. - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var thirdTypeInfo, out _, out _); Assert.That(thirdTypeInfo, Is.Not.SameAs(typeInfo)); } @@ -764,7 +764,7 @@ public void DataTypeName_prioritized_over_NpgsqlDbType([Values]bool generic) DataTypeName = "text", Value = "value" }; - param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions); + param.ResolveTypeInfo(DataSource.CurrentReloadableState.SerializerOptions, null); param.GetResolutionInfo(out var typeInfo, out _, out _); Assert.That(typeInfo, Is.Not.Null); Assert.That(typeInfo.PgTypeId, Is.EqualTo(DataSource.CurrentReloadableState.SerializerOptions.TextPgTypeId)); diff --git a/test/Npgsql.Tests/Support/TestBase.cs b/test/Npgsql.Tests/Support/TestBase.cs index 61c4e2accf..198e10a326 100644 --- a/test/Npgsql.Tests/Support/TestBase.cs +++ b/test/Npgsql.Tests/Support/TestBase.cs @@ -148,7 +148,8 @@ public async Task AssertTypeWrite( bool skipArrayCheck = false) { await using var connection = await OpenConnectionAsync(); - await AssertTypeWrite(connection, valueFactory, expectedSqlLiteral, pgTypeName, npgsqlDbType, dbType, inferredDbType, isDefault, isNpgsqlDbTypeInferredFromClrType, skipArrayCheck); + await AssertTypeWrite(connection, valueFactory, expectedSqlLiteral, pgTypeName, npgsqlDbType, dbType, inferredDbType, isDefault, + isNpgsqlDbTypeInferredFromClrType, skipArrayCheck); } internal static async Task AssertTypeRead( @@ -254,7 +255,7 @@ await AssertTypeWriteCore( } } - internal static async Task AssertTypeWriteCore( + static async Task AssertTypeWriteCore( NpgsqlConnection connection, Func valueFactory, string expectedSqlLiteral, @@ -263,12 +264,10 @@ internal static async Task AssertTypeWriteCore( DbType? dbType = null, DbType? inferredDbType = null, bool isDefault = true, - bool isNpgsqlDbTypeInferredFromClrType = true) + bool isDataTypeInferredFromClrType = true) { if (npgsqlDbType is null) - isNpgsqlDbTypeInferredFromClrType = false; - - inferredDbType ??= isNpgsqlDbTypeInferredFromClrType ? dbType ?? DbType.Object : DbType.Object; + isDataTypeInferredFromClrType = false; // TODO: Interferes with both multiplexing and connection-specific mapping (used e.g. in NodaTime) // Reset the type mapper to make sure we're resolving this type with a clean slate (for isolation, just in case) @@ -281,6 +280,10 @@ internal static async Task AssertTypeWriteCore( ? pgTypeName[..parenIndex] + pgTypeName[(pgTypeName.IndexOf(')') + 1)..] : pgTypeName; + // For composite type with dots in name, Postgresql returns name with quotes - scheme."My.type.name" + // but for npgsql mapping we should use names without quotes - scheme.My.type.name + var pgTypeNameWithoutFacetsAndQuotes = pgTypeNameWithoutFacets.Replace("\"", string.Empty); + // We test the following scenarios (between 2 and 5 in total): // 1. With NpgsqlDbType explicitly set // 2. With DataTypeName explicitly set @@ -293,6 +296,13 @@ internal static async Task AssertTypeWriteCore( await using var cmd = new NpgsqlCommand { Connection = connection }; NpgsqlParameter p; + + // With data type name + p = new NpgsqlParameter { Value = valueFactory(), DataTypeName = pgTypeNameWithoutFacetsAndQuotes }; + cmd.Parameters.Add(p); + errorIdentifier[++errorIdentifierIndex] = $"DataTypeName={pgTypeNameWithoutFacetsAndQuotes}"; + CheckInference(); + // With NpgsqlDbType if (npgsqlDbType is not null) { @@ -302,22 +312,13 @@ internal static async Task AssertTypeWriteCore( CheckInference(); } - // With data type name - // For composite type with dots in name, Postgresql returns name with quotes - scheme."My.type.name" - // but for npgsql mapping we should use names without quotes - scheme.My.type.name - var pgTypeNameWithoutFacetsAndDots = pgTypeNameWithoutFacets.Replace("\"", string.Empty); - p = new NpgsqlParameter { Value = valueFactory(), DataTypeName = pgTypeNameWithoutFacetsAndDots }; - cmd.Parameters.Add(p); - errorIdentifier[++errorIdentifierIndex] = $"DataTypeName={pgTypeNameWithoutFacetsAndDots}"; - CheckInference(); - // With DbType if (dbType is not null) { p = new NpgsqlParameter { Value = valueFactory(), DbType = dbType.Value }; cmd.Parameters.Add(p); errorIdentifier[++errorIdentifierIndex] = $"DbType={dbType}"; - CheckInference(); + CheckInference(dbTypeApplied: true); } if (isDefault) @@ -326,13 +327,13 @@ internal static async Task AssertTypeWriteCore( p = new NpgsqlParameter { Value = valueFactory() }; cmd.Parameters.Add(p); errorIdentifier[++errorIdentifierIndex] = $"Value only (type {p.Value!.GetType().Name}, non-generic)"; - CheckInference(valueOnlyInference: true); + CheckInference(valueSolelyApplied: true); // With (generic) value only p = new NpgsqlParameter { TypedValue = valueFactory() }; cmd.Parameters.Add(p); errorIdentifier[++errorIdentifierIndex] = $"Value only (type {p.Value!.GetType().Name}, generic)"; - CheckInference(valueOnlyInference: true); + CheckInference(valueSolelyApplied: true); } Debug.Assert(cmd.Parameters.Count == errorIdentifierIndex + 1); @@ -349,20 +350,25 @@ internal static async Task AssertTypeWriteCore( Assert.That(reader[i+1], Is.EqualTo(expectedSqlLiteral), $"Got wrong SQL literal when writing with {errorIdentifier[i / 2]}"); } - void CheckInference(bool valueOnlyInference = false) + void CheckInference(bool dbTypeApplied = false, bool valueSolelyApplied = false) { - if (isNpgsqlDbTypeInferredFromClrType && npgsqlDbType is not null) - { - Assert.That(p.NpgsqlDbType, Is.EqualTo(npgsqlDbType), + if (!valueSolelyApplied || isDataTypeInferredFromClrType) + Assert.That(p.DataTypeName, Is.EqualTo(pgTypeNameWithoutFacetsAndQuotes), + () => $"Got wrong inferred DataTypeName when inferring with {errorIdentifier[errorIdentifierIndex]}"); + + if (!valueSolelyApplied || isDataTypeInferredFromClrType) + Assert.That(p.NpgsqlDbType, Is.EqualTo(npgsqlDbType ?? NpgsqlDbType.Unknown), () => $"Got wrong inferred NpgsqlDbType when inferring with {errorIdentifier[errorIdentifierIndex]}"); - } - Assert.That(p.DbType, Is.EqualTo(valueOnlyInference ? inferredDbType : isNpgsqlDbTypeInferredFromClrType ? inferredDbType : dbType ?? DbType.Object), + DbType expectedDbType; + if (dbTypeApplied) + expectedDbType = dbType.GetValueOrDefault(); + else if (!valueSolelyApplied || isDataTypeInferredFromClrType) + expectedDbType = inferredDbType ?? dbType ?? DbType.Object; + else + expectedDbType = DbType.Object; + Assert.That(p.DbType, Is.EqualTo(expectedDbType), () => $"Got wrong inferred DbType when inferring with {errorIdentifier[errorIdentifierIndex]}"); - - if (isNpgsqlDbTypeInferredFromClrType) - Assert.That(p.DataTypeName, Is.EqualTo(pgTypeNameWithoutFacets), - () => $"Got wrong inferred DataTypeName when inferring with {errorIdentifier[errorIdentifierIndex]}"); } } diff --git a/test/Npgsql.Tests/TypeMapperTests.cs b/test/Npgsql.Tests/TypeMapperTests.cs index d0d1e36587..2819c80810 100644 --- a/test/Npgsql.Tests/TypeMapperTests.cs +++ b/test/Npgsql.Tests/TypeMapperTests.cs @@ -1,9 +1,12 @@ using Npgsql.Internal; using NUnit.Framework; using System; +using System.Data; using System.Threading.Tasks; using Npgsql.Internal.Converters; using Npgsql.Internal.Postgres; +using Npgsql.TypeMapping; +using NpgsqlTypes; using static Npgsql.Tests.TestUtil; namespace Npgsql.Tests; @@ -58,6 +61,96 @@ public async Task String_to_citext() Assert.That(command.ExecuteScalar(), Is.True); } + [Test] + [NonParallelizable] // Depends on citext which could be dropped concurrently + public async Task String_to_citext_with_db_type_string() + { + await using var adminConnection = await OpenConnectionAsync(); + await EnsureExtensionAsync(adminConnection, "citext"); + + var dataSourceBuilder = CreateDataSourceBuilder(); + ((INpgsqlTypeMapper)dataSourceBuilder).AddDbTypeResolverFactory(new ForceStringToCitextResolverFactory()); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + await using var command = new NpgsqlCommand("SELECT @p = 'hello'::citext", connection); + var parameter = new NpgsqlParameter("p", DbType.String) + { + Value = "HeLLo" + }; + command.Parameters.Add(parameter); + + Assert.That(command.ExecuteScalar(), Is.True); + Assert.That(parameter.DbType, Is.EqualTo(DbType.String)); + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Citext)); + Assert.That(parameter.DataTypeName, Is.EqualTo("citext")); + } + + [Test] + public async Task Guid_to_custom_type() + { + await using var adminConnection = await OpenConnectionAsync(); + var type = await GetTempTypeName(adminConnection); + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.AddTypeInfoResolverFactory(new GuidTextConverterFactory(type)); + ((INpgsqlTypeMapper)dataSourceBuilder).AddDbTypeResolverFactory(new GuidTextDbTypeResolverFactory(type)); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + await connection.ExecuteNonQueryAsync($"CREATE TYPE {type}"); + await connection.ExecuteNonQueryAsync($""" + -- Input: cstring -> Custom type + CREATE FUNCTION {type}_in(cstring) + RETURNS {type} + AS 'textin' + LANGUAGE internal IMMUTABLE STRICT; + + -- Output: Custom type -> cstring + CREATE FUNCTION {type}_out({type}) + RETURNS cstring + AS 'textout' + LANGUAGE internal IMMUTABLE STRICT; + + -- 3️⃣ Create wrappers for binary I/O + CREATE FUNCTION {type}_recv(internal) + RETURNS {type} + AS 'textrecv' + LANGUAGE internal IMMUTABLE STRICT; + + CREATE FUNCTION {type}_send({type}) + RETURNS bytea + AS 'textsend' + LANGUAGE internal IMMUTABLE STRICT; + """); + + await connection.ExecuteNonQueryAsync($""" + CREATE TYPE {type} ( + internallength = variable, + input = {type}_in, + output = {type}_out, + receive = {type}_recv, + send = {type}_send, + alignment = int4 + ); + CREATE CAST ({type} AS text) WITH INOUT AS IMPLICIT; + """); + await connection.ReloadTypesAsync(); + + var guid = Guid.NewGuid(); + await using var command = new NpgsqlCommand($"SELECT @p::text = '{guid}'", connection); + var parameter = new NpgsqlParameter("p", DbType.Guid) + { + Value = guid + }; + command.Parameters.Add(parameter); + + Assert.That(command.ExecuteScalar(), Is.True); + Assert.That(parameter.DbType, Is.EqualTo(DbType.Guid)); + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown)); + Assert.That(parameter.DataTypeName, Is.EqualTo(type)); + } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4582")] [NonParallelizable] // Drops extension public async Task Type_in_non_default_schema() @@ -109,6 +202,81 @@ sealed class Resolver : IPgTypeInfoResolver } + class ForceStringToCitextResolverFactory : DbTypeResolverFactory + { + public override IDbTypeResolver CreateDbTypeResolver(NpgsqlDatabaseInfo databaseInfo) => new DbTypeResolver(); + + sealed class DbTypeResolver : IDbTypeResolver + { + public string? GetDataTypeName(DbType dbType, Type? type) + { + if (dbType == DbType.String) + return "citext"; + + return null; + } + + public DbType? GetDbType(DataTypeName dataTypeName) + { + if (dataTypeName.UnqualifiedName == "citext") + return DbType.String; + + return null; + } + } + } + + class GuidTextConverterFactory(string typeName) : PgTypeInfoResolverFactory + { + public override IPgTypeInfoResolver? CreateArrayResolver() => null; + public override IPgTypeInfoResolver CreateResolver() => new GuidTextTypeInfoResolver(typeName); + + sealed class GuidTextTypeInfoResolver(string typeName) : IPgTypeInfoResolver + { + public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + { + if (type == typeof(Guid) || dataTypeName?.UnqualifiedName == typeName) + if (options.DatabaseInfo.TryGetPostgresTypeByName(typeName, out var pgType)) + return new(options, new GuidTextConverter(options.TextEncoding), options.ToCanonicalTypeId(pgType)); + + return null; + } + } + + sealed class GuidTextConverter(System.Text.Encoding encoding) : StringBasedTextConverter(encoding) + { + public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements) + { + bufferRequirements = BufferRequirements.None; + return format is DataFormat.Text; + } + protected override Guid ConvertFrom(string value) => Guid.Parse(value); + protected override ReadOnlyMemory ConvertTo(Guid value) => value.ToString().AsMemory(); + } + } + + class GuidTextDbTypeResolverFactory(string typeName) : DbTypeResolverFactory + { + public override IDbTypeResolver CreateDbTypeResolver(NpgsqlDatabaseInfo databaseInfo) => new DbTypeResolver(typeName); + + sealed class DbTypeResolver(string typeName) : IDbTypeResolver + { + public string? GetDataTypeName(DbType dbType, Type? type) + { + if (dbType == DbType.Guid) + return typeName; + return null; + } + + public DbType? GetDbType(DataTypeName dataTypeName) + { + if (dataTypeName == typeName) + return DbType.Guid; + return null; + } + } + } + enum Mood { Sad, Ok, Happy } #endregion Support diff --git a/test/Npgsql.Tests/Types/DateTimeInfinityTests.cs b/test/Npgsql.Tests/Types/DateTimeInfinityTests.cs index 7a4876e47c..147f7f1be9 100644 --- a/test/Npgsql.Tests/Types/DateTimeInfinityTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeInfinityTests.cs @@ -7,9 +7,9 @@ namespace Npgsql.Tests.Types; -[TestFixture(true)] -#if DEBUG [TestFixture(false)] +#if DEBUG +[TestFixture(true)] [NonParallelizable] #endif public sealed class DateTimeInfinityTests : TestBase, IDisposable @@ -70,7 +70,7 @@ public Task TimestampTz_DateTime(DateTime dateTime, string sqlLiteral, string in => AssertType(new(dateTime.Ticks, DateTimeKind.Utc), DisableDateTimeInfinityConversions ? sqlLiteral : infinityConvertedSqlLiteral, "timestamp with time zone", NpgsqlDbType.TimestampTz, DbType.DateTime, DbType.DateTime, comparer: MaxValuePrecisionLenientComparer, - isDefault: true, isNpgsqlDbTypeInferredFromClrType: false); + isDefault: true); [Test, TestCaseSource(nameof(TimestampTzDateTimeOffsetValues))] public Task TimestampTz_DateTimeOffset(DateTimeOffset dateTime, string sqlLiteral, string infinityConvertedSqlLiteral) diff --git a/test/Npgsql.Tests/Types/MiscTypeTests.cs b/test/Npgsql.Tests/Types/MiscTypeTests.cs index 7f1fe7c0ba..143e3d0f07 100644 --- a/test/Npgsql.Tests/Types/MiscTypeTests.cs +++ b/test/Npgsql.Tests/Types/MiscTypeTests.cs @@ -198,13 +198,4 @@ public async Task Void() await using var conn = await OpenConnectionAsync(); Assert.That(await conn.ExecuteScalarAsync("SELECT pg_sleep(0)"), Is.SameAs(null)); } - - [Test, IssueLink("https://github.com/npgsql/npgsql/issues/1364")] - public async Task Unsupported_DbType() - { - await using var conn = await OpenConnectionAsync(); - await using var cmd = new NpgsqlCommand("SELECT @p", conn); - Assert.That(() => cmd.Parameters.Add(new NpgsqlParameter("p", DbType.UInt32) { Value = 8u }), - Throws.Exception.TypeOf()); - } } From 70ad2946a27f607a06ff4d030955e72004e337ef Mon Sep 17 00:00:00 2001 From: Etienne Lafarge Date: Tue, 18 Nov 2025 20:44:27 +0100 Subject: [PATCH 135/155] Fix idle/busy conn. pool metrics when using NpgsqlDataSource (#5497) Fixes #4798 --- src/Npgsql/NpgsqlConnection.cs | 15 +--- src/Npgsql/NpgsqlDataSource.cs | 8 ++ src/Npgsql/NpgsqlEventSource.cs | 130 ++++++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index 6bda4f71d5..be827cc51b 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -220,20 +220,7 @@ void SetupDataSource() _cloningInstantiator = s => new NpgsqlConnection(s); _dataSource = PoolManager.Pools.GetOrAdd(canonical, newDataSource); - if (_dataSource == newDataSource) - { - Debug.Assert(_dataSource is not MultiHostDataSourceWrapper); - // If the pool we created was the one that ended up being stored we need to increment the appropriate counter. - // Avoids a race condition where multiple threads will create a pool but only one will be stored. - if (_dataSource is NpgsqlMultiHostDataSource multiHostConnectorPool) - foreach (var hostPool in multiHostConnectorPool.Pools) - NpgsqlEventSource.Log.DataSourceCreated(hostPool); - else - { - NpgsqlEventSource.Log.DataSourceCreated(newDataSource); - } - } - else + if (_dataSource != newDataSource) newDataSource.Dispose(); // If this is a multi-host data source and the user specified a TargetSessionAttributes, create a wrapper in front of the diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 000d3f1ae3..78d0ca95c8 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -92,6 +92,7 @@ private protected readonly Dictionary> _pendi readonly SemaphoreSlim _setupMappingsSemaphore = new(1); readonly INpgsqlNameTranslator _defaultNameTranslator; + IDisposable? _eventSourceEvents; internal NpgsqlDataSource( NpgsqlConnectionStringBuilder settings, @@ -314,6 +315,10 @@ internal async Task Bootstrap( serializerOptions: serializerOptions, dbTypeResolver: new ChainDbTypeResolver(resolvers)); + if (!NpgsqlEventSource.Log.TryTrackDataSource(Name, this, out _eventSourceEvents)) + _connectionLogger.LogDebug("NpgsqlEventSource could not start tracking a DataSource, " + + "this can happen if more than one data source uses the same connection string."); + IsBootstrapped = true; } finally @@ -519,6 +524,8 @@ protected virtual void DisposeBase() _periodicPasswordProviderTimer?.Dispose(); MetricsReporter.Dispose(); + _eventSourceEvents?.Dispose(); + // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer // Due to possible concurrent access, which might lead to deadlock // See issue #6115 @@ -549,6 +556,7 @@ protected virtual async ValueTask DisposeAsyncBase() await _periodicPasswordProviderTimer.DisposeAsync().ConfigureAwait(false); MetricsReporter.Dispose(); + _eventSourceEvents?.Dispose(); // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer // Due to possible concurrent access, which might lead to deadlock // See issue #6115 diff --git a/src/Npgsql/NpgsqlEventSource.cs b/src/Npgsql/NpgsqlEventSource.cs index 82475142d2..00cfc1ed31 100644 --- a/src/Npgsql/NpgsqlEventSource.cs +++ b/src/Npgsql/NpgsqlEventSource.cs @@ -1,14 +1,19 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; namespace Npgsql; sealed class NpgsqlEventSource : EventSource { public static readonly NpgsqlEventSource Log = new(); + // A static to keep the CWT values from making themselves uncollectable if they would have a reference through the + // NpgsqlEventSource instance to the CWT table, which they would if this was an instance field. + static readonly NpgsqlEventSourceDataSources DataSourceEvents = new(Log); const string EventSourceName = "Npgsql"; @@ -25,8 +30,6 @@ sealed class NpgsqlEventSource : EventSource PollingCounter? _preparedCommandsRatioCounter; PollingCounter? _poolsCounter; - readonly object _dataSourcesLock = new(); - readonly Dictionary _dataSources = new(); PollingCounter? _multiplexingAverageCommandsPerBatchCounter; PollingCounter? _multiplexingAverageWriteTimePerBatchCounter; @@ -64,7 +67,7 @@ internal void BytesRead(long bytesRead) Interlocked.Add(ref _bytesRead, bytesRead); } - public void CommandStart(string sql) + internal void CommandStart(string sql) { if (IsEnabled()) { @@ -74,7 +77,7 @@ public void CommandStart(string sql) NpgsqlSqlEventSource.Log.CommandStart(sql); } - public void CommandStop() + internal void CommandStop() { if (IsEnabled()) Interlocked.Decrement(ref _currentCommands); @@ -93,13 +96,8 @@ internal void CommandFailed() Interlocked.Increment(ref _failedCommands); } - internal void DataSourceCreated(NpgsqlDataSource dataSource) - { - lock (_dataSourcesLock) - { - _dataSources.Add(dataSource, null); - } - } + internal bool TryTrackDataSource(string name, NpgsqlDataSource dataSource, [NotNullWhen(true)]out IDisposable? untrack) + => DataSourceEvents.TryTrack(name, dataSource, out untrack); internal void MultiplexingBatchSent(int numCommands, long elapsedTicks) { @@ -112,13 +110,7 @@ internal void MultiplexingBatchSent(int numCommands, long elapsedTicks) } } - double GetDataSourceCount() - { - lock (_dataSourcesLock) - { - return _dataSources.Count; - } - } + double GetDataSourceCount() => DataSourceEvents.GetDataSourceCount(); double GetMultiplexingAverageCommandsPerBatch() { @@ -142,7 +134,7 @@ double GetMultiplexingAverageWriteTimePerBatch() protected override void OnEventCommand(EventCommandEventArgs command) { - if (command.Command == EventCommand.Enable) + if (command.Command is EventCommand.Enable) { // Comment taken from RuntimeEventSource in CoreCLR // NOTE: These counters will NOT be disposed on disable command because we may be introducing @@ -207,18 +199,94 @@ protected override void OnEventCommand(EventCommandEventArgs command) DisplayName = "Average write time per multiplexing batch", DisplayUnits = "us" }; - lock (_dataSourcesLock) + + DataSourceEvents.EnableAll(); + } + } +} + +// This is a separate class to avoid accidentally making the CWT instance reachable through the value. +// The EventSource is stored in the counters, part of the value, so the EventSource *must not* reference this instance on an instance field. +// This goes for any state captured by the value, which is why the other state has its own object for the value to reference. +// See https://github.com/dotnet/runtime/issues/12255. +sealed class NpgsqlEventSourceDataSources(EventSource eventSource) +{ + readonly ConditionalWeakTable> _dataSources = new(); + readonly StrongBox<(int DataSourceCount, ConcurrentDictionary DataSourceNames)> _nonCwtState = new((0, new())); + + internal double GetDataSourceCount() => _nonCwtState.Value.DataSourceCount; + + internal bool TryTrack(string name, NpgsqlDataSource dataSource, [NotNullWhen(true)]out IDisposable? untrack) + { + untrack = null; + if (!_nonCwtState.Value.DataSourceNames.TryAdd(name, default)) + return false; + + var lazy = new Lazy( + () => new DataSourceEvents(name: name, dataSource, eventSource, _nonCwtState), + LazyThreadSafetyMode.ExecutionAndPublication); + var tracked = _dataSources.TryAdd(dataSource, lazy); + + if (tracked) + { + Interlocked.Increment(ref _nonCwtState.Value.DataSourceCount); + // We must initialize directly when the event source is already enabled. + if (eventSource.IsEnabled()) + untrack = lazy.Value; + else + untrack = new DataSourceEventsDisposable(lazy); + } + + return tracked; + } + + internal void EnableAll() + { + foreach (var dataSourceKv in _dataSources) + { + _ = dataSourceKv.Value.Value; + } + } + + sealed class DataSourceEventsDisposable(Lazy events) : IDisposable + { + public void Dispose() => events.Value.Dispose(); + } + + sealed class DataSourceEvents : IDisposable + { + readonly string _name; + readonly StrongBox<(int Count, ConcurrentDictionary Names)> _state; + readonly PollingCounter _idleConnections; + readonly PollingCounter _busyConnections; + + int _disposed; + + public DataSourceEvents(string name, NpgsqlDataSource dataSource, EventSource eventSource, StrongBox<(int, ConcurrentDictionary)> state) + { + _name = name; + _state = state; + _idleConnections = new($"idle-connections-{name}", eventSource, () => dataSource.Statistics.Idle) + { + DisplayName = $"Idle Connections [{name}]" + }; + _busyConnections = new($"busy-connections-{name}", eventSource, () => dataSource.Statistics.Busy) { - foreach (var dataSource in _dataSources.Keys) - { - if (!_dataSources[dataSource].HasValue) - { - _dataSources[dataSource] = ( - new PollingCounter($"Idle Connections ({dataSource.Settings.ToStringWithoutPassword()}])", this, () => dataSource.Statistics.Idle), - new PollingCounter($"Busy Connections ({dataSource.Settings.ToStringWithoutPassword()}])", this, () => dataSource.Statistics.Busy)); - } - } - } + DisplayName = $"Busy Connections [{name}]" + }; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) is 1) + return; + + _idleConnections.Dispose(); + _busyConnections.Dispose(); + + Interlocked.Decrement(ref _state.Value.Count); + var success = _state.Value.Names.TryRemove(_name, out _); + Debug.Assert(success); } } } From 302af43fb196d3cfc2d379857d65b484e19c6309 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 19 Nov 2025 15:16:29 +0300 Subject: [PATCH 136/155] Add Timeout property to text COPY operations (#6294) Closes #5758 --- src/Npgsql/Internal/NpgsqlConnector.cs | 4 ++-- src/Npgsql/NpgsqlCommand.cs | 10 --------- src/Npgsql/NpgsqlConnection.cs | 12 +++++------ src/Npgsql/NpgsqlRawCopyStream.cs | 28 ++++++++++++++++++++++++++ src/Npgsql/PublicAPI.Unshipped.txt | 12 +++++++++++ 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index bf6f97c1d9..c7508ac2e6 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -2833,11 +2833,11 @@ UserAction DoStartUserAction(ConnectorState newState, NpgsqlCommand? command, StartCancellableOperation(cancellationToken, attemptPgCancellation); - // We reset the ReadBuffer.Timeout for every user action, so it wouldn't leak from the previous query or action + // We reset the ReadBuffer.Timeout and WriteBuffer.Timeout for every user action, so it wouldn't leak from the previous query or action // For example, we might have successfully cancelled the previous query (so the connection is not broken) // But the next time, we call the Prepare, which doesn't set its own timeout var timeoutSeconds = command?.CommandTimeout ?? Settings.CommandTimeout; - ReadBuffer.Timeout = timeoutSeconds > 0 ? TimeSpan.FromSeconds(timeoutSeconds) : Timeout.InfiniteTimeSpan; + ReadBuffer.Timeout = WriteBuffer.Timeout = timeoutSeconds > 0 ? TimeSpan.FromSeconds(timeoutSeconds) : Timeout.InfiniteTimeSpan; return new UserAction(this); } diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index 5f2cb8832f..a83b939a53 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1041,9 +1041,6 @@ static void ValidateParameterCount(NpgsqlBatchCommand batchCommand) #region Message Creation / Population - void BeginSend(NpgsqlConnector connector) - => connector.WriteBuffer.Timeout = TimeSpan.FromSeconds(CommandTimeout); - internal Task Write(NpgsqlConnector connector, bool async, bool flush, CancellationToken cancellationToken = default) { return (_behavior & CommandBehavior.SchemaOnly) == 0 @@ -1158,8 +1155,6 @@ await connector.WriteParse(batchCommand.FinalCommandText, batchCommand.Statement async Task SendDeriveParameters(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default) { - BeginSend(connector); - var syncCaller = !async; for (var i = 0; i < InternalBatchCommands.Count; i++) { @@ -1178,8 +1173,6 @@ async Task SendDeriveParameters(NpgsqlConnector connector, bool async, Cancellat async Task SendPrepare(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default) { - BeginSend(connector); - var syncCaller = !async; for (var i = 0; i < InternalBatchCommands.Count; i++) { @@ -1227,8 +1220,6 @@ bool ShouldSchedule(ref bool async, int indexOfStatementInBatch) async Task SendClose(NpgsqlConnector connector, bool async, CancellationToken cancellationToken = default) { - BeginSend(connector); - foreach (var batchCommand in InternalBatchCommands) { if (!batchCommand.IsPrepared) @@ -1531,7 +1522,6 @@ internal virtual async ValueTask ExecuteReader(bool async, Com // Instead, all sends for non-first statements are performed asynchronously (even if the user requested sync), // in a special synchronization context to prevents a dependency on the thread pool (which would also trigger // deadlocks). - BeginSend(connector); sendTask = Write(connector, async, flush: true, CancellationToken.None); // The following is a hack. It raises an exception if one was thrown in the first phases diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index be827cc51b..ca8b6ba9fe 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -1241,7 +1241,7 @@ async Task BeginBinaryExport(bool async, string copyToComm /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// - public TextWriter BeginTextImport(string copyFromCommand) + public NpgsqlCopyTextWriter BeginTextImport(string copyFromCommand) => BeginTextImport(async: false, copyFromCommand, CancellationToken.None).GetAwaiter().GetResult(); /// @@ -1256,10 +1256,10 @@ public TextWriter BeginTextImport(string copyFromCommand) /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// - public Task BeginTextImportAsync(string copyFromCommand, CancellationToken cancellationToken = default) + public Task BeginTextImportAsync(string copyFromCommand, CancellationToken cancellationToken = default) => BeginTextImport(async: true, copyFromCommand, cancellationToken); - async Task BeginTextImport(bool async, string copyFromCommand, CancellationToken cancellationToken = default) + async Task BeginTextImport(bool async, string copyFromCommand, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(copyFromCommand); if (!IsValidCopyCommand(copyFromCommand)) @@ -1307,7 +1307,7 @@ async Task BeginTextImport(bool async, string copyFromCommand, Cance /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// - public TextReader BeginTextExport(string copyToCommand) + public NpgsqlCopyTextReader BeginTextExport(string copyToCommand) => BeginTextExport(async: false, copyToCommand, CancellationToken.None).GetAwaiter().GetResult(); /// @@ -1322,10 +1322,10 @@ public TextReader BeginTextExport(string copyToCommand) /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// - public Task BeginTextExportAsync(string copyToCommand, CancellationToken cancellationToken = default) + public Task BeginTextExportAsync(string copyToCommand, CancellationToken cancellationToken = default) => BeginTextExport(async: true, copyToCommand, cancellationToken); - async Task BeginTextExport(bool async, string copyToCommand, CancellationToken cancellationToken = default) + async Task BeginTextExport(bool async, string copyToCommand, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(copyToCommand); if (!IsValidCopyCommand(copyToCommand)) diff --git a/src/Npgsql/NpgsqlRawCopyStream.cs b/src/Npgsql/NpgsqlRawCopyStream.cs index 664c39a1b8..d0cfad82a7 100644 --- a/src/Npgsql/NpgsqlRawCopyStream.cs +++ b/src/Npgsql/NpgsqlRawCopyStream.cs @@ -485,6 +485,20 @@ internal NpgsqlCopyTextWriter(NpgsqlConnector connector, NpgsqlRawCopyStream und throw connector.Break(new Exception("Can't use a binary copy stream for text writing")); } + /// + /// Gets or sets a value, in milliseconds, that determines how long the text writer will attempt to write before timing out. + /// + public int Timeout + { + get => ((NpgsqlRawCopyStream)BaseStream).WriteTimeout; + set + { + var stream = (NpgsqlRawCopyStream)BaseStream; + stream.ReadTimeout = value; + stream.WriteTimeout = value; + } + } + /// /// Cancels and terminates an ongoing import. Any data already written will be discarded. /// @@ -511,6 +525,20 @@ internal NpgsqlCopyTextReader(NpgsqlConnector connector, NpgsqlRawCopyStream und throw connector.Break(new Exception("Can't use a binary copy stream for text reading")); } + /// + /// Gets or sets a value, in milliseconds, that determines how long the text reader will attempt to read before timing out. + /// + public int Timeout + { + get => ((NpgsqlRawCopyStream)BaseStream).ReadTimeout; + set + { + var stream = (NpgsqlRawCopyStream)BaseStream; + stream.ReadTimeout = value; + stream.WriteTimeout = value; + } + } + /// /// Cancels and terminates an ongoing export. /// diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 52f47e43d7..b86cac3908 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -5,6 +5,10 @@ Npgsql.GssEncryptionMode.Disable = 0 -> Npgsql.GssEncryptionMode Npgsql.GssEncryptionMode.Prefer = 1 -> Npgsql.GssEncryptionMode Npgsql.GssEncryptionMode.Require = 2 -> Npgsql.GssEncryptionMode Npgsql.TypeMapping.INpgsqlTypeMapper.AddDbTypeResolverFactory(Npgsql.Internal.DbTypeResolverFactory! factory) -> void +Npgsql.NpgsqlConnection.BeginTextExport(string! copyToCommand) -> Npgsql.NpgsqlCopyTextReader! +Npgsql.NpgsqlConnection.BeginTextExportAsync(string! copyToCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Npgsql.NpgsqlConnection.BeginTextImport(string! copyFromCommand) -> Npgsql.NpgsqlCopyTextWriter! +Npgsql.NpgsqlConnection.BeginTextImportAsync(string! copyFromCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Npgsql.NpgsqlConnection.CloneWithAsync(string! connectionString, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Action? Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void @@ -16,6 +20,10 @@ Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string? Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.set -> void +Npgsql.NpgsqlCopyTextReader.Timeout.get -> int +Npgsql.NpgsqlCopyTextReader.Timeout.set -> void +Npgsql.NpgsqlCopyTextWriter.Timeout.get -> int +Npgsql.NpgsqlCopyTextWriter.Timeout.set -> void Npgsql.NpgsqlDataSourceBuilder.ConfigureTypeLoading(System.Action! configureAction) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlDataSourceBuilder.MapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlDataSourceBuilder! Npgsql.NpgsqlDataSourceBuilder.MapComposite(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlDataSourceBuilder! @@ -123,3 +131,7 @@ NpgsqlTypes.NpgsqlLine.Deconstruct(out double a, out double b, out double c) -> NpgsqlTypes.NpgsqlLSeg.Deconstruct(out NpgsqlTypes.NpgsqlPoint start, out NpgsqlTypes.NpgsqlPoint end) -> void NpgsqlTypes.NpgsqlPoint.Deconstruct(out double x, out double y) -> void NpgsqlTypes.NpgsqlTid.Deconstruct(out uint blockNumber, out ushort offsetNumber) -> void +*REMOVED*Npgsql.NpgsqlConnection.BeginTextExport(string! copyToCommand) -> System.IO.TextReader! +*REMOVED*Npgsql.NpgsqlConnection.BeginTextExportAsync(string! copyToCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +*REMOVED*Npgsql.NpgsqlConnection.BeginTextImport(string! copyFromCommand) -> System.IO.TextWriter! +*REMOVED*Npgsql.NpgsqlConnection.BeginTextImportAsync(string! copyFromCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! From a8b95649beabcd9e6eb3a19894c6a37ceb97c60a Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Wed, 19 Nov 2025 15:54:07 +0300 Subject: [PATCH 137/155] Fix synchronous GSS session encryption and enable it by default (#6324) Followup to #2957 --- src/Npgsql/Internal/NpgsqlConnector.Auth.cs | 6 +- src/Npgsql/Internal/NpgsqlConnector.cs | 168 ++++++++++++-------- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 2 +- src/Npgsql/Util/GSSStream.cs | 2 +- 4 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs index 4d5fccbad5..f837f08026 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.Auth.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.Auth.cs @@ -336,7 +336,11 @@ internal async ValueTask AuthenticateGSS(bool async, CancellationToken cancellat using var authContext = new NegotiateAuthentication(clientOptions); var data = authContext.GetOutgoingBlob(ReadOnlySpan.Empty, out var statusCode)!; - Debug.Assert(statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded); + if (statusCode != 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"); + } await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false); await Flush(async, cancellationToken).ConfigureAwait(false); while (true) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index c7508ac2e6..aa2a996322 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -596,17 +596,27 @@ static async Task OpenCore( bool async, CancellationToken cancellationToken) { - await conn.RawOpen(sslMode, gssEncMode, timeout, async, cancellationToken).ConfigureAwait(false); - timeout.CheckAndApply(conn); - conn.WriteStartupMessage(username); - await conn.Flush(async, cancellationToken).ConfigureAwait(false); + // If we fail to connect to the socket, there is no reason to retry even if SslMode/GssEncryption allows it + await conn.RawOpen(timeout, async, cancellationToken).ConfigureAwait(false); - using var cancellationRegistration = conn.StartCancellableOperation(cancellationToken, attemptPgCancellation: false); try { + await conn.SetupEncryption(sslMode, gssEncMode, timeout, async, cancellationToken).ConfigureAwait(false); + timeout.CheckAndApply(conn); + conn.WriteStartupMessage(username); + await conn.Flush(async, cancellationToken).ConfigureAwait(false); + + using var cancellationRegistration = conn.StartCancellableOperation(cancellationToken, attemptPgCancellation: false); await conn.Authenticate(username, timeout, async, cancellationToken).ConfigureAwait(false); } + // 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 + // In addition, this behavior mimics libpq, where it retries as long as GssEncryptionMode and SslMode allows it catch (Exception e) when + // We might also get here OperationCancelledException/TimeoutException + // But it's fine to fall down and retry because we'll immediately exit with the exact same exception + // // Any error after trying with GSS encryption (gssEncMode == GssEncryptionMode.Prefer || // Auth error with/without SSL @@ -620,9 +630,6 @@ static async Task OpenCore( else sslMode = sslMode == SslMode.Prefer ? SslMode.Disable : SslMode.Require; - cancellationRegistration.Dispose(); - Debug.Assert(!conn.IsBroken); - conn.Cleanup(); // If Prefer was specified and we failed (with SSL), retry without SSL. @@ -696,6 +703,8 @@ internal async ValueTask GSSEncrypt(bool async, bool isRequ default: throw new NpgsqlException($"Received unknown response {response} for GSSEncRequest (expecting G or N)"); case 'N': + if (isRequired) + throw new NpgsqlException("GGS encryption requested. No GSS encryption enabled connection from this host is configured."); return GssEncryptionResult.NegotiateFailure; case 'G': break; @@ -907,7 +916,7 @@ async ValueTask GetUsernameAsyncInternal() } } - async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) + async Task RawOpen(NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { try { @@ -939,59 +948,6 @@ async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlT IsSslEncrypted = false; IsGssEncrypted = false; - - var gssEncryptResult = await TryNegotiateGssEncryption(gssEncryptionMode, async, cancellationToken).ConfigureAwait(false); - if (gssEncryptResult == GssEncryptionResult.Success) - return; - - timeout.CheckAndApply(this); - - if (GetSslNegotiation(Settings) == SslNegotiation.Direct) - { - // We already check that in NpgsqlConnectionStringBuilder.PostProcessAndValidate, but since we also allow environment variables... - if (Settings.SslMode is not SslMode.Require and not SslMode.VerifyCA and not SslMode.VerifyFull) - throw new ArgumentException("SSL Mode has to be Require or higher to be used with direct SSL Negotiation"); - if (gssEncryptResult == GssEncryptionResult.NegotiateFailure) - { - // We can be here only if it's fallback from preferred (but failed) gss encryption - // In this case, direct encryption isn't going to work anymore, so we throw a bogus exception to retry again without gss - // Alternatively, we can instead just go with the usual route of writing SslRequest, ignoring direct ssl - // But this is how libpq works - Debug.Assert(gssEncryptionMode == GssEncryptionMode.Prefer); - // The exception message doesn't matter since we're going to retry again - throw new NpgsqlException(); - } - - await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false); - if (ReadBuffer.ReadBytesLeft > 0) - throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); - } - else if ((sslMode is SslMode.Prefer && DataSource.TransportSecurityHandler.SupportEncryption) || - sslMode is SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull) - { - WriteSslRequest(); - await Flush(async, cancellationToken).ConfigureAwait(false); - - await ReadBuffer.Ensure(1, async).ConfigureAwait(false); - var response = (char)ReadBuffer.ReadByte(); - timeout.CheckAndApply(this); - - switch (response) - { - default: - throw new NpgsqlException($"Received unknown response {response} for SSLRequest (expecting S or N)"); - case 'N': - if (sslMode != SslMode.Prefer) - throw new NpgsqlException("SSL connection requested. No SSL enabled connection from this host is configured."); - break; - case 'S': - await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false); - break; - } - - if (ReadBuffer.ReadBytesLeft > 0) - throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); - } } catch { @@ -1008,12 +964,80 @@ async Task RawOpen(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlT } } + async Task SetupEncryption(SslMode sslMode, GssEncryptionMode gssEncryptionMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) + { + var gssEncryptResult = await TryNegotiateGssEncryption(gssEncryptionMode, async, cancellationToken).ConfigureAwait(false); + if (gssEncryptResult == GssEncryptionResult.Success) + return; + + // TryNegotiateGssEncryption should already throw a much more meaningful exception + // if GSS encryption is required but for some reason we can't negotiate it. + // But since we have to return a specific result instead of generic true/false + // To make absolutely sure we didn't miss anything, recheck again + if (gssEncryptionMode == GssEncryptionMode.Require) + throw new NpgsqlException($"Unable to negotiate GSS encryption: {gssEncryptResult}"); + + timeout.CheckAndApply(this); + + if (GetSslNegotiation(Settings) == SslNegotiation.Direct) + { + // We already check that in NpgsqlConnectionStringBuilder.PostProcessAndValidate, but since we also allow environment variables... + if (Settings.SslMode is not SslMode.Require and not SslMode.VerifyCA and not SslMode.VerifyFull) + throw new ArgumentException("SSL Mode has to be Require or higher to be used with direct SSL Negotiation"); + if (gssEncryptResult == GssEncryptionResult.NegotiateFailure) + { + // We can be here only if it's fallback from preferred (but failed) gss encryption + // In this case, direct encryption isn't going to work anymore, so we throw a bogus exception to retry again without gss + // Alternatively, we can instead just go with the usual route of writing SslRequest, ignoring direct ssl + // But this is how libpq works + Debug.Assert(gssEncryptionMode == GssEncryptionMode.Prefer); + // The exception message doesn't matter since we're going to retry again + throw new NpgsqlException(); + } + + await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false); + if (ReadBuffer.ReadBytesLeft > 0) + throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); + } + else if ((sslMode is SslMode.Prefer && DataSource.TransportSecurityHandler.SupportEncryption) || + sslMode is SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull) + { + WriteSslRequest(); + await Flush(async, cancellationToken).ConfigureAwait(false); + + await ReadBuffer.Ensure(1, async).ConfigureAwait(false); + var response = (char)ReadBuffer.ReadByte(); + timeout.CheckAndApply(this); + + switch (response) + { + default: + throw new NpgsqlException($"Received unknown response {response} for SSLRequest (expecting S or N)"); + case 'N': + if (sslMode != SslMode.Prefer) + throw new NpgsqlException("SSL connection requested. No SSL enabled connection from this host is configured."); + break; + case 'S': + await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false); + break; + } + + if (ReadBuffer.ReadBytesLeft > 0) + throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack."); + } + } + async ValueTask TryNegotiateGssEncryption(GssEncryptionMode gssEncryptionMode, bool async, CancellationToken cancellationToken) { // GetCredentialFailure is essentially a nop (since we didn't send anything over the wire) // So we can proceed further as if gss encryption wasn't even attempted if (gssEncryptionMode == GssEncryptionMode.Disable) return GssEncryptionResult.GetCredentialFailure; + // Same thing as above, though in this case user doesn't require GSS encryption but didn't enable encryption + // Most of the time they're using the default value, in which case also exit without throwing an error + if (gssEncryptionMode == GssEncryptionMode.Prefer && !DataSource.TransportSecurityHandler.SupportEncryption) + return GssEncryptionResult.GetCredentialFailure; + if (ConnectedEndPoint!.AddressFamily == AddressFamily.Unix) { if (gssEncryptionMode == GssEncryptionMode.Prefer) @@ -1038,7 +1062,9 @@ static SslNegotiation GetSslNegotiation(NpgsqlConnectionStringBuilder settings) return sslNegotiation; } - return SslNegotiation.Postgres; + // If user hasn't provided the value via connection string or environment variable + // Retrieve the default value from property + return settings.SslNegotiation; } static GssEncryptionMode GetGssEncMode(NpgsqlConnectionStringBuilder settings) @@ -1052,7 +1078,9 @@ static GssEncryptionMode GetGssEncMode(NpgsqlConnectionStringBuilder settings) return gssEncMode; } - return GssEncryptionMode.Disable; + // If user hasn't provided the value via connection string or environment variable + // Retrieve the default value from property + return settings.GssEncryptionMode; } internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) @@ -2187,9 +2215,13 @@ void DoCancelRequest(int backendProcessId, int backendSecretKey) { try { - RawOpen(Settings.SslMode, gssEncMode, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, + var timeout = new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)); + RawOpen(timeout, false, CancellationToken.None) .GetAwaiter().GetResult(); + SetupEncryption(Settings.SslMode, gssEncMode, timeout, false, + CancellationToken.None). + GetAwaiter().GetResult(); } catch (Exception e) when (gssEncMode == GssEncryptionMode.Prefer) { @@ -2198,9 +2230,13 @@ void DoCancelRequest(int backendProcessId, int backendSecretKey) // If we hit an error with gss encryption // Retry again without it - RawOpen(Settings.SslMode, GssEncryptionMode.Disable, new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)), false, + var timeout = new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)); + RawOpen(timeout, false, CancellationToken.None) .GetAwaiter().GetResult(); + SetupEncryption(Settings.SslMode, GssEncryptionMode.Disable, timeout, false, + CancellationToken.None). + GetAwaiter().GetResult(); } WriteCancelRequest(backendProcessId, backendSecretKey); diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index ca0d734c5f..1e1e87107d 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -488,7 +488,7 @@ public SslNegotiation SslNegotiation [NpgsqlConnectionStringProperty] public GssEncryptionMode GssEncryptionMode { - get => UserProvidedGssEncMode ?? GssEncryptionMode.Disable; + get => UserProvidedGssEncMode ?? GssEncryptionMode.Prefer; set { UserProvidedGssEncMode = value; diff --git a/src/Npgsql/Util/GSSStream.cs b/src/Npgsql/Util/GSSStream.cs index c6c47bd4ca..4f98a1d1fa 100644 --- a/src/Npgsql/Util/GSSStream.cs +++ b/src/Npgsql/Util/GSSStream.cs @@ -59,7 +59,7 @@ public override void Write(ReadOnlySpan buffer) Unsafe.WriteUnaligned(ref _writeLengthBuffer[0], BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(written.Length) : written.Length); _stream.Write(_writeLengthBuffer); - _stream.Write(buffer.Slice(start, lengthToWrite)); + _stream.Write(_writeBuffer.WrittenMemory.Span); _writeBuffer.ResetWrittenCount(); start += lengthToWrite; From fc0a675c67130a77418d5d52e9d266c0afb3597c Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Wed, 19 Nov 2025 17:56:46 +0100 Subject: [PATCH 138/155] Move TryTrackDataSource to constructor and enable metrics more accurately (#6329) --- src/Npgsql/MetricsReporter.cs | 24 +++++++-------- src/Npgsql/MultiHostDataSourceWrapper.cs | 2 +- src/Npgsql/NpgsqlDataSource.cs | 38 +++++++++++++++++------- src/Npgsql/NpgsqlMultiHostDataSource.cs | 5 ++-- src/Npgsql/PoolingDataSource.cs | 2 +- src/Npgsql/UnpooledDataSource.cs | 2 +- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index a25a3173cb..71b43923fe 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -178,20 +178,16 @@ static IEnumerable> GetConnectionUsage() { var reporter = Reporters[i]; - if (reporter._dataSource is PoolingDataSource poolingDataSource) - { - var stats = poolingDataSource.Statistics; - - measurements.Add(new Measurement( - stats.Idle, - reporter._poolNameTag, - new KeyValuePair("state", "idle"))); - - measurements.Add(new Measurement( - stats.Busy, - reporter._poolNameTag, - new KeyValuePair("state", "used"))); - } + var connectionStats = reporter._dataSource.Statistics; + measurements.Add(new Measurement( + connectionStats.Idle, + reporter._poolNameTag, + new KeyValuePair("state", "idle"))); + + measurements.Add(new Measurement( + connectionStats.Busy, + reporter._poolNameTag, + new KeyValuePair("state", "used"))); } return measurements; diff --git a/src/Npgsql/MultiHostDataSourceWrapper.cs b/src/Npgsql/MultiHostDataSourceWrapper.cs index b6b7d3e5f5..432875ae67 100644 --- a/src/Npgsql/MultiHostDataSourceWrapper.cs +++ b/src/Npgsql/MultiHostDataSourceWrapper.cs @@ -8,7 +8,7 @@ namespace Npgsql; sealed class MultiHostDataSourceWrapper(NpgsqlMultiHostDataSource wrappedSource, TargetSessionAttributes targetSessionAttributes) - : NpgsqlDataSource(CloneSettingsForTargetSessionAttributes(wrappedSource.Settings, targetSessionAttributes), wrappedSource.Configuration) + : NpgsqlDataSource(CloneSettingsForTargetSessionAttributes(wrappedSource.Settings, targetSessionAttributes), wrappedSource.Configuration, reportMetrics: false) { internal NpgsqlMultiHostDataSource WrappedSource { get; } = wrappedSource; diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 78d0ca95c8..280b32c128 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -92,11 +92,11 @@ private protected readonly Dictionary> _pendi readonly SemaphoreSlim _setupMappingsSemaphore = new(1); readonly INpgsqlNameTranslator _defaultNameTranslator; - IDisposable? _eventSourceEvents; + readonly IDisposable? _eventSourceEvents; internal NpgsqlDataSource( NpgsqlConnectionStringBuilder settings, - NpgsqlDataSourceConfiguration dataSourceConfig) + NpgsqlDataSourceConfiguration dataSourceConfig, bool reportMetrics) { Settings = settings; ConnectionString = settings.PersistSecurityInfo @@ -145,7 +145,21 @@ internal NpgsqlDataSource( } Name = name ?? ConnectionString; - MetricsReporter = new MetricsReporter(this); + + // TODO this needs a rework, but for now we just avoid tracking multi-host data sources directly. + if (reportMetrics) + { + MetricsReporter = new MetricsReporter(this); + if (!NpgsqlEventSource.Log.TryTrackDataSource(Name, this, out _eventSourceEvents)) + _connectionLogger.LogDebug("NpgsqlEventSource could not start tracking a DataSource, " + + "this can happen if more than one data source uses the same connection string."); + } + else + { + // This is not accessed anywhere currently for multi-host data sources. + // Connectors which handle the metrics always access their nonpooling/pooling data source instead. + MetricsReporter = null!; + } } /// @@ -315,10 +329,6 @@ internal async Task Bootstrap( serializerOptions: serializerOptions, dbTypeResolver: new ChainDbTypeResolver(resolvers)); - if (!NpgsqlEventSource.Log.TryTrackDataSource(Name, this, out _eventSourceEvents)) - _connectionLogger.LogDebug("NpgsqlEventSource could not start tracking a DataSource, " + - "this can happen if more than one data source uses the same connection string."); - IsBootstrapped = true; } finally @@ -523,8 +533,11 @@ protected virtual void DisposeBase() } _periodicPasswordProviderTimer?.Dispose(); - MetricsReporter.Dispose(); - _eventSourceEvents?.Dispose(); + if (MetricsReporter is not null) + { + MetricsReporter.Dispose(); + _eventSourceEvents?.Dispose(); + } // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer // Due to possible concurrent access, which might lead to deadlock @@ -555,8 +568,11 @@ protected virtual async ValueTask DisposeAsyncBase() if (_periodicPasswordProviderTimer is not null) await _periodicPasswordProviderTimer.DisposeAsync().ConfigureAwait(false); - MetricsReporter.Dispose(); - _eventSourceEvents?.Dispose(); + if (MetricsReporter is not null) + { + MetricsReporter.Dispose(); + _eventSourceEvents?.Dispose(); + } // We do not dispose _setupMappingsSemaphore explicitly, leaving it to finalizer // Due to possible concurrent access, which might lead to deadlock // See issue #6115 diff --git a/src/Npgsql/NpgsqlMultiHostDataSource.cs b/src/Npgsql/NpgsqlMultiHostDataSource.cs index 5d21ab8954..4ccc0809b5 100644 --- a/src/Npgsql/NpgsqlMultiHostDataSource.cs +++ b/src/Npgsql/NpgsqlMultiHostDataSource.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -31,7 +30,7 @@ public sealed class NpgsqlMultiHostDataSource : NpgsqlDataSource volatile int _roundRobinIndex = -1; internal NpgsqlMultiHostDataSource(NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig) - : base(settings, dataSourceConfig) + : base(settings, dataSourceConfig, reportMetrics: false) { var hosts = settings.Host!.Split(','); _pools = new NpgsqlDataSource[hosts.Length]; @@ -53,7 +52,7 @@ internal NpgsqlMultiHostDataSource(NpgsqlConnectionStringBuilder settings, Npgsq : new UnpooledDataSource(poolSettings, dataSourceConfig); } - var targetSessionAttributeValues = Enum.GetValues().ToArray(); + var targetSessionAttributeValues = Enum.GetValues(); var highestValue = 0; foreach (var value in targetSessionAttributeValues) if ((int)value > highestValue) diff --git a/src/Npgsql/PoolingDataSource.cs b/src/Npgsql/PoolingDataSource.cs index b2e96d0d4c..18ddc1e63f 100644 --- a/src/Npgsql/PoolingDataSource.cs +++ b/src/Npgsql/PoolingDataSource.cs @@ -74,7 +74,7 @@ internal sealed override (int Total, int Idle, int Busy) Statistics internal PoolingDataSource( NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig) - : base(settings, dataSourceConfig) + : base(settings, dataSourceConfig, reportMetrics: true) { if (settings.MaxPoolSize < settings.MinPoolSize) throw new ArgumentException($"Connection can't have 'Max Pool Size' {settings.MaxPoolSize} under 'Min Pool Size' {settings.MinPoolSize}"); diff --git a/src/Npgsql/UnpooledDataSource.cs b/src/Npgsql/UnpooledDataSource.cs index e801f537eb..55ce5d65af 100644 --- a/src/Npgsql/UnpooledDataSource.cs +++ b/src/Npgsql/UnpooledDataSource.cs @@ -7,7 +7,7 @@ namespace Npgsql; sealed class UnpooledDataSource(NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig) - : NpgsqlDataSource(settings, dataSourceConfig) + : NpgsqlDataSource(settings, dataSourceConfig, reportMetrics: true) { volatile int _numConnectors; From 90f699bb8b0024ec2f0bf648a7106b0104717a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 19 Nov 2025 10:05:44 -0800 Subject: [PATCH 139/155] Define TFM-specific dependencies (#6326) Co-authored-by: Shay Rojansky Co-authored-by: Nino Floris --- Directory.Packages.props | 44 +++++++++++++++---- .../Npgsql.DependencyInjection.csproj | 2 +- src/Npgsql/Npgsql.csproj | 2 + test/Npgsql.Tests/Types/JsonTests.cs | 5 +++ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f8a9e16cd0..d15be7ff34 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,11 +1,39 @@ + + 10.0.0 + 10.0.0 + + + 10.0.0 + 10.0.0 + + + + 9.0.0 + 9.0.0 + + + 9.0.11 + 9.0.11 + + + + 8.0.0 + 8.0.0 + + + 9.0.11 + + + 8.0.1 + 8.0.1 + + - - + + - - - + @@ -24,15 +52,15 @@ - - + + + - diff --git a/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj b/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj index aa33763975..bf502446e1 100644 --- a/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj +++ b/src/Npgsql.DependencyInjection/Npgsql.DependencyInjection.csproj @@ -2,7 +2,7 @@ Shay Rojansky - net8.0 + net8.0 npgsql;postgresql;postgres;ado;ado.net;database;sql;di;dependency injection README.md diff --git a/src/Npgsql/Npgsql.csproj b/src/Npgsql/Npgsql.csproj index 0eab75cd66..01aaa5013d 100644 --- a/src/Npgsql/Npgsql.csproj +++ b/src/Npgsql/Npgsql.csproj @@ -15,6 +15,8 @@ + + diff --git a/test/Npgsql.Tests/Types/JsonTests.cs b/test/Npgsql.Tests/Types/JsonTests.cs index 84b95389bb..9cfb07de8c 100644 --- a/test/Npgsql.Tests/Types/JsonTests.cs +++ b/test/Npgsql.Tests/Types/JsonTests.cs @@ -182,7 +182,12 @@ public Task Roundtrip_JsonObject() [Test] public Task Roundtrip_JsonArray() => AssertType( +#if NET8_0 + // Necessary until we drop STJ 8.0, see https://github.com/dotnet/runtime/pull/103733 + new JsonArray { (JsonValue)1, (JsonValue)2, (JsonValue)3 }, +#else new JsonArray { 1, 2, 3 }, +#endif IsJsonb ? "[1, 2, 3]" : "[1,2,3]", PostgresType, NpgsqlDbType, From a27566ff3e75ab1f75feb6d24cb69cdbd3340ab4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 19 Nov 2025 21:21:25 +0100 Subject: [PATCH 140/155] Align OTel metrics to latest spec (#6328) Closes #6313 --- Directory.Packages.props | 2 + src/Npgsql/MetricsReporter.cs | 85 ++++++++------ test/Npgsql.Tests/MetricTests.cs | 161 ++++++++++++++++++++++++++ test/Npgsql.Tests/Npgsql.Tests.csproj | 2 + test/Npgsql.Tests/TracingTests.cs | 1 - 5 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 test/Npgsql.Tests/MetricTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d15be7ff34..2ca1f14b38 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,6 +61,8 @@ + + diff --git a/src/Npgsql/MetricsReporter.cs b/src/Npgsql/MetricsReporter.cs index 71b43923fe..431c0ea734 100644 --- a/src/Npgsql/MetricsReporter.cs +++ b/src/Npgsql/MetricsReporter.cs @@ -28,7 +28,9 @@ sealed class MetricsReporter : IDisposable static readonly ObservableGauge PreparedRatio; readonly NpgsqlDataSource _dataSource; + readonly KeyValuePair _poolNameTag; + readonly TagList _durationMetricTags; static readonly List Reporters = []; @@ -51,66 +53,68 @@ static MetricsReporter() { Meter = new("Npgsql", Version); + // db.client.operation.duration is stable in the OpenTelemetry spec + CommandDuration = Meter.CreateHistogram( + "db.client.operation.duration", + unit: "s", + description: "Duration of database client operations.", + advice: ShortHistogramAdvice); + + // From here, metrics have "development" status (not stable) + Meter.CreateObservableUpDownCounter( + "db.client.connection.count", + GetConnectionCount, + unit: "{connection}", + description: "The number of connections that are currently in state described by the state attribute."); + + // It's a bit ridiculous to manage "max connections" as an observable counter, given that it never changes for a given pool. + // However, we can't simply report it once at startup, since clients who connect later wouldn't have it. And since reporting it + // repeatedly isn't possible because we need to provide incremental figures, we just manage it as an observable counter. + Meter.CreateObservableUpDownCounter( + "db.client.connection.max", + GetConnectionMax, + unit: "{connection}", + description: "The maximum number of open connections allowed."); + + // From here, metrics are entirely Npgsql-specific and not covered by the OpenTelemetry spec. CommandsExecuting = Meter.CreateUpDownCounter( - "db.client.commands.executing", + "db.client.operation.npgsql.executing", unit: "{command}", description: "The number of currently executing database commands."); CommandsFailed = Meter.CreateCounter( - "db.client.commands.failed", + "db.client.operation.failed", unit: "{command}", description: "The number of database commands which have failed."); - CommandDuration = Meter.CreateHistogram( - "db.client.commands.duration", - unit: "s", - description: "The duration of database commands, in seconds.", - advice: ShortHistogramAdvice); - BytesWritten = Meter.CreateCounter( - "db.client.commands.bytes_written", + "db.client.operation.npgsql.bytes_written", unit: "By", description: "The number of bytes written."); BytesRead = Meter.CreateCounter( - "db.client.commands.bytes_read", + "db.client.operation.npgsql.bytes_read", unit: "By", description: "The number of bytes read."); PendingConnectionRequests = Meter.CreateUpDownCounter( - "db.client.connections.pending_requests", + "db.client.connection.npgsql.pending_requests", unit: "{request}", description: "The number of pending requests for an open connection, cumulative for the entire pool."); ConnectionTimeouts = Meter.CreateCounter( - "db.client.connections.timeouts", + "db.client.connection.npgsql.timeouts", unit: "{timeout}", description: "The number of connection timeouts that have occurred trying to obtain a connection from the pool."); ConnectionCreateTime = Meter.CreateHistogram( - "db.client.connections.create_time", + "db.client.connection.npgsql.create_time", unit: "s", description: "The time it took to create a new connection.", advice: ShortHistogramAdvice); - // Observable metrics; these are for values we already track internally (and efficiently) inside the connection pool implementation. - Meter.CreateObservableUpDownCounter( - "db.client.connections.usage", - GetConnectionUsage, - unit: "{connection}", - description: "The number of connections that are currently in state described by the state attribute."); - - // It's a bit ridiculous to manage "max connections" as an observable counter, given that it never changes for a given pool. - // However, we can't simply report it once at startup, since clients who connect later wouldn't have it. And since reporting it - // repeatedly isn't possible because we need to provide incremental figures, we just manage it as an observable counter. - Meter.CreateObservableUpDownCounter( - "db.client.connections.max", - GetMaxConnections, - unit: "{connection}", - description: "The maximum number of open connections allowed."); - PreparedRatio = Meter.CreateObservableGauge( - "db.client.commands.prepared_ratio", + "db.client.operation.npgsql.prepared_ratio", GetPreparedCommandsRatio, description: "The ratio of prepared command executions."); } @@ -118,7 +122,16 @@ static MetricsReporter() public MetricsReporter(NpgsqlDataSource dataSource) { _dataSource = dataSource; - _poolNameTag = new KeyValuePair("pool.name", dataSource.Name); + _poolNameTag = new KeyValuePair("db.client.connection.pool.name", dataSource.Name); + + _durationMetricTags = new TagList + { + // TODO: Vary this for PG-like databases (e.g. CockroachDB)? + { "db.system.name", "postgresql" }, + { "db.client.connection.pool.name", _dataSource.Name }, + { "server.address", _dataSource.Settings.Host }, + { "server.port", _dataSource.Settings.Port } + }; lock (Reporters) { @@ -142,7 +155,7 @@ internal void ReportCommandStop(long startTimestamp) if (CommandDuration.Enabled && startTimestamp > 0) { - CommandDuration.Record(Stopwatch.GetElapsedTime(startTimestamp).TotalSeconds, _poolNameTag); + CommandDuration.Record(Stopwatch.GetElapsedTime(startTimestamp).TotalSeconds, _durationMetricTags); } } @@ -168,7 +181,7 @@ internal void ReportPendingConnectionRequestStop() internal void ReportConnectionCreateTime(TimeSpan duration) => ConnectionCreateTime.Record(duration.TotalSeconds, _poolNameTag); - static IEnumerable> GetConnectionUsage() + static IEnumerable> GetConnectionCount() { lock (Reporters) { @@ -182,19 +195,19 @@ static IEnumerable> GetConnectionUsage() measurements.Add(new Measurement( connectionStats.Idle, reporter._poolNameTag, - new KeyValuePair("state", "idle"))); + new KeyValuePair("db.client.connection.state", "idle"))); measurements.Add(new Measurement( connectionStats.Busy, reporter._poolNameTag, - new KeyValuePair("state", "used"))); + new KeyValuePair("db.client.connection.state", "used"))); } return measurements; } } - static IEnumerable> GetMaxConnections() + static IEnumerable> GetConnectionMax() { lock (Reporters) { diff --git a/test/Npgsql.Tests/MetricTests.cs b/test/Npgsql.Tests/MetricTests.cs new file mode 100644 index 0000000000..235f8b4e27 --- /dev/null +++ b/test/Npgsql.Tests/MetricTests.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +namespace Npgsql.Tests; + +public class MetricTests : TestBase +{ + [Test] + public async Task OperationDuration() + { + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("Npgsql") + .AddInMemoryExporter(exportedItems) + .Build(); + + await using var dataSource = CreateDataSource(); + await using var conn = await dataSource.OpenConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + await using (var reader = await cmd.ExecuteReaderAsync()) + while (await reader.ReadAsync()); + + meterProvider.ForceFlush(); + + var metric = exportedItems.SingleOrDefault(m => m.Name == "db.client.operation.duration"); + Assert.That(metric, Is.Not.Null, "Metric 'db.client.operation.duration' not found."); + + var point = GetFilteredPoints(metric.GetMetricPoints(), dataSource.Name).Single(); + + Assert.That(point.GetHistogramSum(), Is.GreaterThan(0)); + Assert.That(point.GetHistogramCount(), Is.EqualTo(1)); + + var tags = ToDictionary(point.Tags); + + using (Assert.EnterMultipleScope()) + { + // TODO: Vary this for PG-like databases (e.g. CockroachDB)? + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + + Assert.That(tags["server.address"], Is.EqualTo(dataSource.Settings.Host)); + Assert.That(tags["server.port"], Is.EqualTo(dataSource.Settings.Port)); + Assert.That(tags["db.client.connection.pool.name"], Is.EqualTo(dataSource.Name)); + } + } + + [Test] + public async Task ConnectionCount() + { + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("Npgsql") + .AddInMemoryExporter(exportedItems) + .Build(); + + await using var dataSource = CreateDataSource(); + + using (var _ = await dataSource.OpenConnectionAsync()) + { + meterProvider.ForceFlush(); + + var metric = exportedItems.Single(m => m.Name == "db.client.connection.count"); + var points = GetFilteredPoints(metric.GetMetricPoints(), dataSource.Name); + + var usedPoint = GetPoint(points, "used"); + Assert.That(usedPoint.GetSumLong(), Is.EqualTo(1), "Expected used connections to be 1"); + + var idlePoint = GetPoint(points, "idle"); + Assert.That(idlePoint.GetSumLong(), Is.Zero, "Expected idle connections to be 0"); + + exportedItems.Clear(); + } + + meterProvider.ForceFlush(); + + { + var metric = exportedItems.Single(m => m.Name == "db.client.connection.count"); + var points = GetFilteredPoints(metric.GetMetricPoints(), dataSource.Name); + + var usedPoint = GetPoint(points, "used"); + Assert.That(usedPoint.GetSumLong(), Is.Zero, "Expected used connections to be 0"); + + var idlePoint = GetPoint(points, "idle"); + Assert.That(idlePoint.GetSumLong(), Is.EqualTo(1), "Expected idle connections to be 1"); + } + + static MetricPoint GetPoint(IEnumerable points, string state) + { + foreach (var point in points) + { + foreach (var tag in point.Tags) + { + if (tag.Key == "db.client.connection.state" && (string?)tag.Value == state) + return point; + } + } + + Assert.Fail($"Point with state '{state}' not found"); + throw new UnreachableException(); + } + } + + [Test] + public async Task ConnectionMax() + { + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("Npgsql") + .AddInMemoryExporter(exportedItems) + .Build(); + + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.MaxPoolSize = 134; + 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(p => p.GetSumLong() == 134); + var tags = ToDictionary(point.Tags); + Assert.That(tags["db.client.connection.pool.name"], Is.EqualTo(dataSource.Name)); + } + + static Dictionary ToDictionary(ReadOnlyTagCollection tags) + { + var dict = new Dictionary(); + foreach (var tag in tags) + dict[tag.Key] = tag.Value; + return dict; + } + + protected override NpgsqlDataSourceBuilder CreateDataSourceBuilder() + { + var dataSourceBuilder = base.CreateDataSourceBuilder(); + dataSourceBuilder.Name = "MetricsDataSource" + Interlocked.Increment(ref _dataSourceCounter); + return dataSourceBuilder; + } + + protected override NpgsqlDataSource CreateDataSource() + => CreateDataSourceBuilder().Build(); + + int _dataSourceCounter; + + static IEnumerable GetFilteredPoints(MetricPointsAccessor points, string dataSourceName) + { + foreach (var point in points) + { + foreach (var tag in point.Tags) + { + if (tag.Key == "db.client.connection.pool.name" && (string?)tag.Value == dataSourceName) + yield return point; + } + } + } +} diff --git a/test/Npgsql.Tests/Npgsql.Tests.csproj b/test/Npgsql.Tests/Npgsql.Tests.csproj index 2944755e6c..3714b9edaa 100644 --- a/test/Npgsql.Tests/Npgsql.Tests.csproj +++ b/test/Npgsql.Tests/Npgsql.Tests.csproj @@ -9,6 +9,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index faec49c238..fe7464f0ce 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Net.Sockets; using System.Threading.Tasks; using NUnit.Framework; From c15f00a0f86fc5dba2ff2a4fffa807ee0287bf92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:12:14 +0100 Subject: [PATCH 141/155] Bump Microsoft.CodeAnalysis.CSharp from 4.14.0 to 5.0.0 (#6330) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2ca1f14b38..52becfa8c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + From 572671e949d6a69a75f8d1a9cd79c0ddc898d608 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 20 Nov 2025 13:42:55 +0300 Subject: [PATCH 142/155] Fix failing Timeout_during_authentication test (#6332) --- test/Npgsql.Tests/AuthenticationTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/Npgsql.Tests/AuthenticationTests.cs b/test/Npgsql.Tests/AuthenticationTests.cs index 157b1ee287..1503d7f373 100644 --- a/test/Npgsql.Tests/AuthenticationTests.cs +++ b/test/Npgsql.Tests/AuthenticationTests.cs @@ -368,9 +368,8 @@ public async Task Timeout_during_authentication() // request. This should trigger a timeout await using var dataSource = CreateDataSource(postmasterMock.ConnectionString); await using var connection = dataSource.CreateConnection(); - Assert.That(async () => await connection.OpenAsync(), - Throws.Exception.TypeOf() - .With.InnerException.TypeOf()); + var ex = Assert.ThrowsAsync(async () => await connection.OpenAsync()); + Assert.That(ex.InnerException, Is.TypeOf()); } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/1180")] From 41c5f40a8ecc1e3e0da680c2f700ccb953590607 Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Thu, 20 Nov 2025 16:59:11 +0300 Subject: [PATCH 143/155] Complete fix for failing Timeout_during_authentication test (#6334) --- src/Npgsql/Internal/NpgsqlConnector.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index aa2a996322..774d19c9f7 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -1278,8 +1278,6 @@ void Connect(NpgsqlTimeout timeout) } } - timeout.Check(); - // Give each endpoint an equal share of the remaining time var perEndpointTimeout = -1; // Default to infinity if (timeout.IsSet) @@ -1400,15 +1398,7 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok using var combinedCts = endpointTimeout.IsSet ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : null; combinedCts?.CancelAfter(endpointTimeout.CheckAndGetTimeLeft()); var combinedToken = combinedCts?.Token ?? cancellationToken; - try - { - await socket.ConnectAsync(endpoint, combinedToken).ConfigureAwait(false); - } - catch (OperationCanceledException oce) when ( - oce.CancellationToken == combinedToken && !cancellationToken.IsCancellationRequested) - { - throw new TimeoutException(); - } + await socket.ConnectAsync(endpoint, combinedToken).ConfigureAwait(false); _socket = socket; ConnectedEndPoint = endpoint; @@ -1429,6 +1419,8 @@ async Task ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationTok if (e is OperationCanceledException) e = new TimeoutException("Timeout during connection attempt"); + else if (e is NpgsqlException) + e = e.InnerException!; // We throw NpgsqlException for timeouts, wrapping TimeoutException ConnectionLogger.LogTrace(e, "Failed to connect to {Endpoint}", endpoint); From b8b7f34fb99db8f96cfc8606fb3fe8df5c130ead Mon Sep 17 00:00:00 2001 From: kevbot18 <4250416+kevbot18@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:32:02 -0500 Subject: [PATCH 144/155] Throw ObjectDisposedException when assigining to NpgsqlCommand (#6048) --- src/Npgsql/NpgsqlCommand.cs | 23 ++++++++++++++++++----- test/Npgsql.Tests/CommandTests.cs | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index a83b939a53..f1767d6e76 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -183,8 +183,18 @@ public override string CommandText { Debug.Assert(WrappingBatch is null); - if (State != CommandState.Idle) - ThrowHelper.ThrowInvalidOperationException("An open data reader exists for this command."); + switch (State) + { + case CommandState.Idle: + break; + case CommandState.Disposed: + ThrowHelper.ThrowObjectDisposedException(typeof(NpgsqlCommand).FullName); + break; + case CommandState.InProgress: + default: + ThrowHelper.ThrowInvalidOperationException("An open data reader exists for this command."); + break; + } _commandText = value ?? string.Empty; @@ -252,9 +262,12 @@ protected override DbConnection? DbConnection if (InternalConnection == value) return; - InternalConnection = State == CommandState.Idle - ? (NpgsqlConnection?)value - : throw new InvalidOperationException("An open data reader exists for this command."); + InternalConnection = State switch + { + CommandState.Idle => (NpgsqlConnection?)value, + CommandState.Disposed => throw new ObjectDisposedException(typeof(NpgsqlCommand).FullName), + _ => throw new InvalidOperationException("An open data reader exists for this command."), + }; Transaction = null; } diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs index 584a3cc433..70488be12e 100644 --- a/test/Npgsql.Tests/CommandTests.cs +++ b/test/Npgsql.Tests/CommandTests.cs @@ -1694,4 +1694,18 @@ public async Task Completed_transaction_throws([Values] bool commit) Assert.Throws(() => cmd.Transaction = tx); } + + [Test, Description("Writing to properties of a disposed command raises ObjectDisposedException.")] + public async Task Disposed_command_throws_on_assignment() + { + await using var conn = await OpenConnectionAsync(); + var command = new NpgsqlCommand("SELECT 1"); + command.Dispose(); + + Assert.Throws(() => command.Connection = conn); + Assert.Throws(() => command.CommandText = "SELECT 2"); + + Assert.That(command.Connection, Is.Null); + Assert.That(command.CommandText, Is.EqualTo("SELECT 1")); + } } From ed05ab2ad3217970e972d07b5af2eb92bb8d093b Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Thu, 20 Nov 2025 20:52:52 +0100 Subject: [PATCH 145/155] Override GetColumnSchemaAsync (#6337) Fixes #6017 --- src/Npgsql/NpgsqlDataReader.cs | 14 +++++++------- src/Npgsql/PublicAPI.Unshipped.txt | 2 ++ src/Npgsql/Schema/DbColumnSchemaGenerator.cs | 11 ++++++----- test/Npgsql.Tests/ReaderNewSchemaTests.cs | 6 ++++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 2421e7e5ca..94499c14ae 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -1771,7 +1771,7 @@ public override IEnumerator GetEnumerator() /// /// public ReadOnlyCollection GetColumnSchema() - => GetColumnSchema(async: false).GetAwaiter().GetResult(); + => GetColumnSchema(async: false).GetAwaiter().GetResult(); ReadOnlyCollection IDbColumnSchemaGenerator.GetColumnSchema() { @@ -1788,14 +1788,14 @@ ReadOnlyCollection IDbColumnSchemaGenerator.GetColumnSchema() /// Asynchronously returns schema information for the columns in the current resultset. /// /// - public new Task> GetColumnSchemaAsync(CancellationToken cancellationToken = default) - => GetColumnSchema(async: true, cancellationToken); + public override Task> GetColumnSchemaAsync(CancellationToken cancellationToken = default) + => GetColumnSchema(async: true, cancellationToken); - Task> GetColumnSchema(bool async, CancellationToken cancellationToken = default) + Task> GetColumnSchema(bool async, CancellationToken cancellationToken = default) where T : DbColumn => RowDescription == null || ColumnCount == 0 - ? Task.FromResult(new List().AsReadOnly()) + ? Task.FromResult(new List().AsReadOnly()) : new DbColumnSchemaGenerator(_connection!, RowDescription, _behavior.HasFlag(CommandBehavior.KeyInfo)) - .GetColumnSchema(async, cancellationToken); + .GetColumnSchema(async, cancellationToken); #endregion @@ -1853,7 +1853,7 @@ Task> GetColumnSchema(bool async, Cancellatio table.Columns.Add("ProviderSpecificDataType", typeof(Type)); table.Columns.Add("DataTypeName", typeof(string)); - foreach (var column in await GetColumnSchema(async, cancellationToken).ConfigureAwait(false)) + foreach (var column in await GetColumnSchema(async, cancellationToken).ConfigureAwait(false)) { var row = table.NewRow(); diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index b86cac3908..9cea3e5609 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -87,6 +87,7 @@ Npgsql.Replication.PgOutput.PgOutputStreamingMode.Parallel = 2 -> Npgsql.Replica Npgsql.SslNegotiation Npgsql.SslNegotiation.Direct = 1 -> Npgsql.SslNegotiation Npgsql.SslNegotiation.Postgres = 0 -> Npgsql.SslNegotiation +override Npgsql.NpgsqlDataReader.GetColumnSchemaAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! override Npgsql.NpgsqlMultiHostDataSource.Clear() -> void Npgsql.NpgsqlDataSource.ReloadTypes() -> void Npgsql.NpgsqlDataSource.ReloadTypesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -135,3 +136,4 @@ NpgsqlTypes.NpgsqlTid.Deconstruct(out uint blockNumber, out ushort offsetNumber) *REMOVED*Npgsql.NpgsqlConnection.BeginTextExportAsync(string! copyToCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! *REMOVED*Npgsql.NpgsqlConnection.BeginTextImport(string! copyFromCommand) -> System.IO.TextWriter! *REMOVED*Npgsql.NpgsqlConnection.BeginTextImportAsync(string! copyFromCommand, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +*REMOVED*Npgsql.NpgsqlDataReader.GetColumnSchemaAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! diff --git a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs index 458dc725fc..ed7afd822b 100644 --- a/src/Npgsql/Schema/DbColumnSchemaGenerator.cs +++ b/src/Npgsql/Schema/DbColumnSchemaGenerator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -93,13 +94,13 @@ nspname NOT IN ('pg_catalog', 'information_schema') AND #endregion Column queries - internal async Task> GetColumnSchema(bool async, CancellationToken cancellationToken = default) + internal async Task> GetColumnSchema(bool async, CancellationToken cancellationToken = default) where T : DbColumn { // This is mainly for Amazon Redshift var oldQueryMode = _connection.PostgreSqlVersion < new Version(8, 2); var numFields = _rowDescription.Count; - var result = new List(numFields); + var result = new List(numFields); for (var i = 0; i < numFields; i++) result.Add(null); var populatedColumns = 0; @@ -153,7 +154,7 @@ internal async Task> GetColumnSchema(bool asy // The column's ordinal is with respect to the resultset, not its table column.ColumnOrdinal = ordinal; - result[ordinal] = column; + result[ordinal] = (T?)(object)column; } } } @@ -172,14 +173,14 @@ internal async Task> GetColumnSchema(bool asy // Fill in whatever info we have from the RowDescription itself for (var i = 0; i < numFields; i++) { - var column = result[i]; + var column = (NpgsqlDbColumn?)(object?)result[i]; var field = _rowDescription[i]; if (column is null) { column = SetUpNonColumnField(field); column.ColumnOrdinal = i; - result[i] = column; + result[i] = (T?)(object)column; populatedColumns++; } diff --git a/test/Npgsql.Tests/ReaderNewSchemaTests.cs b/test/Npgsql.Tests/ReaderNewSchemaTests.cs index f892670d96..2b28501aeb 100644 --- a/test/Npgsql.Tests/ReaderNewSchemaTests.cs +++ b/test/Npgsql.Tests/ReaderNewSchemaTests.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Npgsql.PostgresTypes; using NUnit.Framework; @@ -809,6 +811,6 @@ class SomeComposite public int Foo { get; set; } } - async Task> GetColumnSchema(NpgsqlDataReader reader) - => IsAsync ? await reader.GetColumnSchemaAsync() : reader.GetColumnSchema(); + async Task> GetColumnSchema(NpgsqlDataReader reader) + => IsAsync ? (await reader.GetColumnSchemaAsync(CancellationToken.None)).Cast().ToArray() : reader.GetColumnSchema(); } From 937a9bd223f5956a9772284496c31127670bcc0e Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 20 Nov 2025 22:15:06 +0100 Subject: [PATCH 146/155] Align with the stable OTel tracing specs (#6338) Closes #6064 --- src/Npgsql/Internal/NpgsqlConnector.cs | 5 +- src/Npgsql/NpgsqlActivitySource.cs | 128 +++++++------- src/Npgsql/NpgsqlCommand.cs | 2 +- test/Npgsql.Tests/TracingTests.cs | 227 +++++++++++-------------- 4 files changed, 168 insertions(+), 194 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index 774d19c9f7..a90da65959 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -498,7 +498,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca { var username = await GetUsernameAsync(async, cancellationToken).ConfigureAwait(false); - activity = NpgsqlActivitySource.ConnectionOpen(this); + activity = NpgsqlActivitySource.PhysicalConnectionOpen(this); var gssEncMode = GetGssEncMode(Settings); @@ -572,8 +572,7 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca } } - if (activity is not null) - NpgsqlActivitySource.CommandStop(activity); + activity?.Dispose(); LogMessages.OpenedPhysicalConnection( ConnectionLogger, Host, Port, Database, UserFacingConnectionString, diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index e40ae5a9bd..8c41cbfca1 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -8,6 +8,8 @@ namespace Npgsql; +// Semantic conventions for database client spans: https://opentelemetry.io/docs/specs/semconv/database/database-spans/ +// Semantic conventions for PostgreSQL client operations: https://opentelemetry.io/docs/specs/semconv/database/postgresql/ static class NpgsqlActivitySource { static readonly ActivitySource Source = new("Npgsql", GetLibraryVersion()); @@ -16,64 +18,69 @@ static class NpgsqlActivitySource internal static Activity? CommandStart(NpgsqlConnectionStringBuilder settings, string commandText, CommandType commandType, string? spanName) { - var dbName = settings.Database ?? "UNKNOWN"; - string? dbOperation = null; - string? dbSqlTable = null; - string activityName; + string? operationName = null; + switch (commandType) { case CommandType.StoredProcedure: - dbOperation = NpgsqlCommand.EnableStoredProcedureCompatMode ? "SELECT" : "CALL"; - // In this case our activity name follows the concept of the CommandType.TableDirect case - // (" .") but replaces db.sql.table with the procedure name - // which seems to match the spec's intent without being explicitly specified that way (it suggests - // using the procedure name but doesn't mention using db.operation or db.name in that case). - activityName = $"{dbOperation} {dbName}.{commandText}"; + // We follow the {db.operation.name} {target} pattern of the spec, with the operation being SELECT/CALL and + // the target being the stored procedure name. + operationName = NpgsqlCommand.EnableStoredProcedureCompatMode ? "SELECT" : "CALL"; + spanName ??= $"{operationName} {commandText}"; break; case CommandType.TableDirect: - dbOperation = "SELECT"; - // The OpenTelemetry spec actually asks to include the database name into db.sql.table - // but then again mixes the concept of database and schema. - // As I interpret it, it actually wants db.sql.table to include the schema name and not the - // database name if the concept of schemas exists in the database system. - // This also makes sense in the context of the activity name which otherwise would include the - // database name twice. - dbSqlTable = commandText; - activityName = $"{dbOperation} {dbName}.{dbSqlTable}"; + // We follow the {db.operation.name} {target} pattern of the spec, with the operation being SELECT and + // the target being the table (collection) name. + operationName = "SELECT"; + spanName ??= $"{operationName} {commandText}"; break; case CommandType.Text: - activityName = dbName; + // We don't have db.query.summary, db.operation.name or target (without parsing SQL), + // so we fall back to db.system.name as per the specs. + spanName ??= "postgresql"; break; default: throw new ArgumentOutOfRangeException(nameof(commandType), commandType, null); } - var activity = Source.StartActivity(spanName ?? activityName, ActivityKind.Client); + var activity = Source.StartActivity(spanName, ActivityKind.Client); if (activity is not { IsAllDataRequested: true }) return activity; - activity.SetTag("db.statement", commandText); + activity.SetTag("db.query.text", commandText); - if (dbOperation != null) - activity.SetTag("db.operation", dbOperation); - if (dbSqlTable != null) - activity.SetTag("db.sql.table", dbSqlTable); + switch (commandType) + { + case CommandType.StoredProcedure: + Debug.Assert(operationName is not null); + activity.SetTag("db.operation.name", operationName); + activity.SetTag("db.stored_procedure.name", commandText); + break; + case CommandType.TableDirect: + Debug.Assert(operationName is not null); + activity.SetTag("db.operation.name", operationName); + activity.SetTag("db.collection.name", commandText); + break; + } return activity; } - internal static Activity? ConnectionOpen(NpgsqlConnector connector) + internal static Activity? PhysicalConnectionOpen(NpgsqlConnector connector) { if (!connector.DataSource.Configuration.TracingOptions.EnablePhysicalOpenTracing) return null; + // Note that physical connection open is not part of the OpenTelemetry spec. + // We emit it if enabled, following the general name/tags guidelines. var dbName = connector.Settings.Database ?? connector.InferredUserName; - var activity = Source.StartActivity(dbName, ActivityKind.Client); + var activity = Source.StartActivity("CONNECT " + dbName, ActivityKind.Client); if (activity is not { IsAllDataRequested: true }) return activity; - activity.SetTag("db.system", "postgresql"); - activity.SetTag("db.connection_string", connector.UserFacingConnectionString); + // We set these basic tags on the activity so that they're populated even when the physical open fails. + activity.SetTag("db.system.name", "postgresql"); + activity.SetTag("db.npgsql.data_source", connector.DataSource.Name); return activity; } @@ -83,34 +90,33 @@ internal static void Enrich(Activity activity, NpgsqlConnector connector) if (!activity.IsAllDataRequested) return; - activity.SetTag("db.system", "postgresql"); - activity.SetTag("db.connection_string", connector.UserFacingConnectionString); - activity.SetTag("db.user", connector.InferredUserName); - // We trace the actual (maybe inferred) database name we're connected to, even if it - // wasn't specified in the connection string - activity.SetTag("db.name", connector.Settings.Database ?? connector.InferredUserName); - activity.SetTag("db.connection_id", connector.Id); + activity.SetTag("db.system.name", "postgresql"); + + // TODO: For now, we only set the database name, without adding the first schema in the search_path + // as per the PG tracing specs (https://opentelemetry.io/docs/specs/semconv/database/postgresql/). + // See #6336 + activity.SetTag("db.namespace", connector.Settings.Database ?? connector.InferredUserName); var endPoint = connector.ConnectedEndPoint; Debug.Assert(endPoint is not null); + activity.SetTag("server.address", connector.Host); switch (endPoint) { case IPEndPoint ipEndPoint: - activity.SetTag("net.transport", "ip_tcp"); - activity.SetTag("net.peer.ip", ipEndPoint.Address.ToString()); if (ipEndPoint.Port != 5432) - activity.SetTag("net.peer.port", ipEndPoint.Port); - activity.SetTag("net.peer.name", connector.Host); + activity.SetTag("server.port", ipEndPoint.Port); break; case UnixDomainSocketEndPoint: - activity.SetTag("net.transport", "unix"); - activity.SetTag("net.peer.name", connector.Host); break; default: - throw new ArgumentOutOfRangeException("Invalid endpoint type: " + endPoint.GetType()); + throw new UnreachableException("Invalid endpoint type: " + endPoint.GetType()); } + + // Npgsql-specific tags + activity.SetTag("db.npgsql.data_source", connector.DataSource.Name); + activity.SetTag("db.npgsql.connection_id", connector.Id); } internal static void ReceivedFirstResponse(Activity activity, NpgsqlTracingOptions tracingOptions) @@ -122,25 +128,27 @@ internal static void ReceivedFirstResponse(Activity activity, NpgsqlTracingOptio activity.AddEvent(activityEvent); } - internal static void CommandStop(Activity activity) + internal static void SetException(Activity activity, Exception exception, bool escaped = true) { - activity.SetStatus(ActivityStatusCode.Ok); - activity.Dispose(); - } + activity.AddException(exception); - internal static void SetException(Activity activity, Exception ex, bool escaped = true) - { - // TODO: We can instead use Activity.AddException whenever we start using .NET 9 - var tags = new ActivityTagsCollection + if (exception is PostgresException { SqlState: var sqlState }) { - { "exception.type", ex.GetType().FullName }, - { "exception.message", ex.Message }, - { "exception.stacktrace", ex.ToString() }, - { "exception.escaped", escaped } - }; - var activityEvent = new ActivityEvent("exception", tags: tags); - activity.AddEvent(activityEvent); - var statusDescription = ex is PostgresException pgEx ? pgEx.SqlState : ex.Message; + activity.SetTag("db.response.status_code", sqlState); + + // error.type SHOULD match the db.response.status_code returned by the database or the client library, or the canonical name of exception that occurred. + // Since we don't have a table to map the error code to a textual description, the SQL state is the best we can do. + activity.SetTag("error.type", sqlState); + } + else + { + if (exception is NpgsqlException { InnerException: Exception innerException }) + exception = innerException; + + activity.SetTag("error.type", exception.GetType().FullName); + } + + var statusDescription = exception is PostgresException pgEx ? pgEx.SqlState : exception.Message; activity.SetStatus(ActivityStatusCode.Error, statusDescription); activity.Dispose(); } diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index f1767d6e76..cfd48189ec 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1787,7 +1787,7 @@ internal void TraceCommandStop() { if (CurrentActivity is not null) { - NpgsqlActivitySource.CommandStop(CurrentActivity); + CurrentActivity.Dispose(); CurrentActivity = null; } } diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index fe7464f0ce..da7f038d4a 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -17,10 +17,12 @@ public async Task Basic_open([Values] bool async) var activities = new List(); - using var activityListener = new ActivityListener(); - activityListener.ShouldListenTo = source => source.Name == "Npgsql"; - activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; - activityListener.ActivityStopped = activity => activities.Add(activity); + using var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Add(activity) + }; ActivitySource.AddActivityListener(activityListener); await using var dataSource = CreateDataSource(); @@ -28,7 +30,7 @@ public async Task Basic_open([Values] bool async) ? await dataSource.OpenConnectionAsync() : dataSource.OpenConnection(); - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); ValidateActivity(activities[0], conn, IsMultiplexing); if (!IsMultiplexing) @@ -41,7 +43,7 @@ public async Task Basic_open([Values] bool async) await conn.ExecuteScalarAsync("SELECT 1"); - Assert.That(activities.Count, Is.EqualTo(2)); + Assert.That(activities, Has.Count.EqualTo(2)); ValidateActivity(activities[0], conn, IsMultiplexing); // For multiplexing, query's activity can be considered as a parent for physical open's activity @@ -49,36 +51,26 @@ public async Task Basic_open([Values] bool async) static void ValidateActivity(Activity activity, NpgsqlConnection conn, bool isMultiplexing) { - Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); - Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); - Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Ok)); + Assert.That(activity.DisplayName, Is.EqualTo("CONNECT " + conn.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo("CONNECT " + conn.Settings.Database)); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Unset)); Assert.That(activity.Events.Count(), Is.EqualTo(0)); - var expectedTagCount = conn.Settings.Port == 5432 ? 8 : 9; - Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); - - Assert.That(activity.TagObjects.Any(x => x.Key == "db.statement"), Is.False); + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Has.Count.EqualTo(conn.Settings.Port == 5432 ? 5 : 6)); - var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); - Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(conn.Settings.Database)); - var userTag = activity.TagObjects.First(x => x.Key == "db.user"); - Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + Assert.That(tags, Does.Not.ContainKey("db.query.text")); - var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); - Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(conn.ConnectionString)); - var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); - Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); - - if (!isMultiplexing) - { - var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); - Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); - } + if (isMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); else - Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(conn.ProcessID)); } } @@ -90,56 +82,48 @@ public async Task Basic_query([Values] bool async, [Values] bool batch) var activities = new List(); - using var activityListener = new ActivityListener(); - activityListener.ShouldListenTo = source => source.Name == "Npgsql"; - activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; - activityListener.ActivityStopped = activity => activities.Add(activity); + using var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Add(activity) + }; ActivitySource.AddActivityListener(activityListener); - await using var dataSource = CreateDataSource(); + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.Name = "TestTracingDataSource"; + await using var dataSource = dataSourceBuilder.Build(); await using var conn = await dataSource.OpenConnectionAsync(); // We're not interested in physical open's activity - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); activities.Clear(); await ExecuteScalar(conn, async, batch, "SELECT 42"); - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); - Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); - Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Ok)); + Assert.That(activity.DisplayName, Is.EqualTo("postgresql")); + Assert.That(activity.OperationName, Is.EqualTo("postgresql")); + Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Unset)); Assert.That(activity.Events.Count(), Is.EqualTo(1)); var firstResponseEvent = activity.Events.First(); Assert.That(firstResponseEvent.Name, Is.EqualTo("received-first-response")); - var expectedTagCount = conn.Settings.Port == 5432 ? 9 : 10; - Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); - - var queryTag = activity.TagObjects.First(x => x.Key == "db.statement"); - Assert.That(queryTag.Value, Is.EqualTo("SELECT 42")); - - var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); - Assert.That(systemTag.Value, Is.EqualTo("postgresql")); - - var userTag = activity.TagObjects.First(x => x.Key == "db.user"); - Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Has.Count.EqualTo(conn.Settings.Port == 5432 ? 6 : 7)); - var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); - Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); + Assert.That(tags["db.query.text"], Is.EqualTo("SELECT 42")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(conn.Settings.Database)); - var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); - Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo("TestTracingDataSource")); - if (!IsMultiplexing) - { - var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); - Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); - } + if (IsMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); else - Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(conn.ProcessID)); } [Test] @@ -150,10 +134,12 @@ public async Task Error_open([Values] bool async) var activities = new List(); - using var activityListener = new ActivityListener(); - activityListener.ShouldListenTo = source => source.Name == "Npgsql"; - activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; - activityListener.ActivityStopped = activity => activities.Add(activity); + using var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Add(activity) + }; ActivitySource.AddActivityListener(activityListener); await using var dataSource = CreateDataSource(x => x.Host = "not-existing-host"); @@ -164,10 +150,10 @@ public async Task Error_open([Values] bool async) : dataSource.OpenConnection(); })!; - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo(dataSource.Settings.Database)); - Assert.That(activity.OperationName, Is.EqualTo(dataSource.Settings.Database)); + Assert.That(activity.DisplayName, Is.EqualTo("CONNECT " + dataSource.Settings.Database)); + Assert.That(activity.OperationName, Is.EqualTo("CONNECT " + dataSource.Settings.Database)); Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); Assert.That(activity.StatusDescription, Is.EqualTo(ex.Message)); @@ -175,27 +161,20 @@ public async Task Error_open([Values] bool async) var exceptionEvent = activity.Events.First(); Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); - Assert.That(exceptionEvent.Tags.Count(), Is.EqualTo(4)); - - var exceptionTypeTag = exceptionEvent.Tags.First(x => x.Key == "exception.type"); - Assert.That(exceptionTypeTag.Value, Is.EqualTo(ex.GetType().FullName)); - - var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); - Assert.That((string)exceptionMessageTag.Value!, Does.Contain(ex.Message)); + var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); + Assert.That(exceptionTags, Has.Count.EqualTo(3)); - var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); - Assert.That((string)exceptionStacktraceTag.Value!, Does.Contain(ex.Message)); + Assert.That(exceptionTags["exception.type"], Is.EqualTo(ex.GetType().FullName)); + Assert.That(exceptionTags["exception.message"], Does.Contain(ex.Message)); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain(ex.Message)); - var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); - Assert.That(exceptionEscapedTag.Value, Is.True); + var activityTags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(activityTags, Has.Count.EqualTo(3)); - Assert.That(activity.TagObjects.Count(), Is.EqualTo(2)); + Assert.That(activityTags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(dataSource.ConnectionString)); - var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); - Assert.That(systemTag.Value, Is.EqualTo("postgresql")); - - var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); - Assert.That(connStringTag.Value, Is.EqualTo(dataSource.ConnectionString)); + Assert.That(activityTags["error.type"], Is.EqualTo("System.Net.Sockets.SocketException")); } [Test] @@ -206,10 +185,12 @@ public async Task Error_query([Values] bool async, [Values] bool batch) var activities = new List(); - using var activityListener = new ActivityListener(); - activityListener.ShouldListenTo = source => source.Name == "Npgsql"; - activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; - activityListener.ActivityStopped = activity => activities.Add(activity); + using var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Add(activity) + }; ActivitySource.AddActivityListener(activityListener); await using var dataSource = CreateDataSource(); @@ -221,10 +202,10 @@ public async Task Error_query([Values] bool async, [Values] bool batch) Assert.ThrowsAsync(async () => await ExecuteScalar(conn, async, batch, "SELECT * FROM non_existing_table")); - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo(conn.Settings.Database)); - Assert.That(activity.OperationName, Is.EqualTo(conn.Settings.Database)); + Assert.That(activity.DisplayName, Is.EqualTo("postgresql")); + Assert.That(activity.OperationName, Is.EqualTo("postgresql")); Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); Assert.That(activity.StatusDescription, Is.EqualTo(PostgresErrorCodes.UndefinedTable)); @@ -232,45 +213,29 @@ public async Task Error_query([Values] bool async, [Values] bool batch) var exceptionEvent = activity.Events.First(); Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); - Assert.That(exceptionEvent.Tags.Count(), Is.EqualTo(4)); - - var exceptionTypeTag = exceptionEvent.Tags.First(x => x.Key == "exception.type"); - Assert.That(exceptionTypeTag.Value, Is.EqualTo("Npgsql.PostgresException")); - - var exceptionMessageTag = exceptionEvent.Tags.First(x => x.Key == "exception.message"); - Assert.That((string)exceptionMessageTag.Value!, Does.Contain("relation \"non_existing_table\" does not exist")); - - var exceptionStacktraceTag = exceptionEvent.Tags.First(x => x.Key == "exception.stacktrace"); - Assert.That((string)exceptionStacktraceTag.Value!, Does.Contain("relation \"non_existing_table\" does not exist")); + var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); + Assert.That(exceptionTags, Has.Count.EqualTo(3)); - var exceptionEscapedTag = exceptionEvent.Tags.First(x => x.Key == "exception.escaped"); - Assert.That(exceptionEscapedTag.Value, Is.True); + Assert.That(exceptionTags["exception.type"], Is.EqualTo("Npgsql.PostgresException")); + Assert.That(exceptionTags["exception.message"], Does.Contain("relation \"non_existing_table\" does not exist")); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain("relation \"non_existing_table\" does not exist")); - var expectedTagCount = conn.Settings.Port == 5432 ? 9 : 10; - Assert.That(activity.TagObjects.Count(), Is.EqualTo(expectedTagCount)); + var activityTags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(activityTags, Has.Count.EqualTo(conn.Settings.Port == 5432 ? 8 : 9)); - var queryTag = activity.TagObjects.First(x => x.Key == "db.statement"); - Assert.That(queryTag.Value, Is.EqualTo("SELECT * FROM non_existing_table")); + Assert.That(activityTags["db.query.text"], Is.EqualTo("SELECT * FROM non_existing_table")); + Assert.That(activityTags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(activityTags["db.namespace"], Is.EqualTo(conn.Settings.Database)); - var systemTag = activity.TagObjects.First(x => x.Key == "db.system"); - Assert.That(systemTag.Value, Is.EqualTo("postgresql")); + Assert.That(activityTags["db.response.status_code"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + Assert.That(activityTags["error.type"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); - var userTag = activity.TagObjects.First(x => x.Key == "db.user"); - Assert.That(userTag.Value, Is.EqualTo(conn.Settings.Username)); + Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(conn.ConnectionString)); - var dbNameTag = activity.TagObjects.First(x => x.Key == "db.name"); - Assert.That(dbNameTag.Value, Is.EqualTo(conn.Settings.Database)); - - var connStringTag = activity.TagObjects.First(x => x.Key == "db.connection_string"); - Assert.That(connStringTag.Value, Is.EqualTo(conn.ConnectionString)); - - if (!IsMultiplexing) - { - var connIDTag = activity.TagObjects.First(x => x.Key == "db.connection_id"); - Assert.That(connIDTag.Value, Is.EqualTo(conn.ProcessID)); - } + if (IsMultiplexing) + Assert.That(activityTags, Does.ContainKey("db.npgsql.connection_id")); else - Assert.That(activity.TagObjects.Any(x => x.Key == "db.connection_id")); + Assert.That(activityTags["db.npgsql.connection_id"], Is.EqualTo(conn.ProcessID)); } [Test] @@ -281,10 +246,12 @@ public async Task Configure_tracing([Values] bool async, [Values] bool batch) var activities = new List(); - using var activityListener = new ActivityListener(); - activityListener.ShouldListenTo = source => source.Name == "Npgsql"; - activityListener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded; - activityListener.ActivityStopped = activity => activities.Add(activity); + using var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Add(activity) + }; ActivitySource.AddActivityListener(activityListener); var dataSourceBuilder = CreateDataSourceBuilder(); @@ -308,19 +275,19 @@ public async Task Configure_tracing([Values] bool async, [Values] bool batch) await ExecuteScalar(conn, async, batch, "SELECT 1"); - Assert.That(activities.Count, Is.EqualTo(0)); + Assert.That(activities, Is.Empty); await ExecuteScalar(conn, async, batch, "SELECT 2"); - Assert.That(activities.Count, Is.EqualTo(1)); + Assert.That(activities, Has.Count.EqualTo(1)); var activity = activities[0]; Assert.That(activity.DisplayName, Is.EqualTo("unknown_query")); Assert.That(activity.OperationName, Is.EqualTo("unknown_query")); Assert.That(activity.Events.Count(), Is.EqualTo(0)); - var customTag = activity.TagObjects.First(x => x.Key == "custom_tag"); - Assert.That(customTag.Value, Is.EqualTo("custom_value")); + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags["custom_tag"], Is.EqualTo("custom_value")); } async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query) From 7b6ac490c735748677dea3c8f760bedc5b973ba4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:16:13 +0100 Subject: [PATCH 147/155] Bump actions/checkout from 5 to 6 (#6344) --- .github/workflows/build.yml | 8 ++++---- .github/workflows/native-aot.yml | 6 +++--- .github/workflows/rich-code-nav.yml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1eff7639e1..14e7ad6875 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: NuGet Cache uses: actions/cache@v4 @@ -146,7 +146,7 @@ jobs: sudo -u postgres psql -c "CREATE USER npgsql_tests_scram SUPERUSER PASSWORD 'npgsql_tests_scram'" # Uncomment the following to SSH into the agent running the build (https://github.com/mxschmitt/action-tmate) - #- uses: actions/checkout@v5 + #- uses: actions/checkout@v6 #- name: Setup tmate session # uses: mxschmitt/action-tmate@v3 @@ -327,7 +327,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: NuGet Cache uses: actions/cache@v4 @@ -367,7 +367,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup .NET Core SDK uses: actions/setup-dotnet@v5.0.0 diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 3cb3ab007a..7e92384ea2 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -93,7 +93,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 # - name: Setup nuget config # run: echo "$nuget_config" > NuGet.config @@ -127,7 +127,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 # - name: Setup nuget config # run: echo "$nuget_config" > NuGet.config @@ -154,7 +154,7 @@ jobs: shell: bash # Uncomment the following to SSH into the agent running the build (https://github.com/mxschmitt/action-tmate) - #- uses: actions/checkout@v5 + #- uses: actions/checkout@v6 #- name: Setup tmate session # uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/rich-code-nav.yml b/.github/workflows/rich-code-nav.yml index 363eaaeb5d..e6842c11ca 100644 --- a/.github/workflows/rich-code-nav.yml +++ b/.github/workflows/rich-code-nav.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: NuGet Cache uses: actions/cache@v4 From 7f7296d1d7056efa66a97d397bafe048dd2da2df Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Thu, 20 Nov 2025 23:23:05 +0100 Subject: [PATCH 148/155] Make Include Realm the default (#6341) Closes #4628 --- src/Npgsql/NpgsqlConnectionStringBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Npgsql/NpgsqlConnectionStringBuilder.cs b/src/Npgsql/NpgsqlConnectionStringBuilder.cs index 1e1e87107d..9dd2696e51 100644 --- a/src/Npgsql/NpgsqlConnectionStringBuilder.cs +++ b/src/Npgsql/NpgsqlConnectionStringBuilder.cs @@ -614,6 +614,7 @@ public string KerberosServiceName [Category("Security")] [Description("The Kerberos realm to be used for authentication.")] [DisplayName("Include Realm")] + [DefaultValue(true)] [NpgsqlConnectionStringProperty] public bool IncludeRealm { From 6d297236532b8d2b219f8727f238b18f2d810c51 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Fri, 21 Nov 2025 00:33:58 +0100 Subject: [PATCH 149/155] Re-enable skipped macOS tests (#6340) --- test/Npgsql.Tests/ConnectionTests.cs | 5 +---- test/Npgsql.Tests/MultipleHostsTests.cs | 1 - .../Replication/CommonLogicalReplicationTests.cs | 1 - test/Npgsql.Tests/Replication/CommonReplicationTests.cs | 1 - test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs | 1 - .../Npgsql.Tests/Replication/TestDecodingReplicationTests.cs | 1 - 6 files changed, 1 insertion(+), 9 deletions(-) diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index e64b3ba982..2eed87dab3 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -130,7 +130,6 @@ public async Task Broken_lifecycle([Values] bool openFromClose) } [Test] - [Platform(Exclude = "MacOsX", Reason = "Flaky on MacOS")] public async Task Break_while_open() { if (IsMultiplexing) @@ -818,7 +817,6 @@ public async Task No_database_defaults_to_username() } [Test, Description("Breaks a connector while it's in the pool, with a keepalive and without")] - [Platform(Exclude = "MacOsX", Reason = "Fails only on mac, needs to be investigated")] [TestCase(false, TestName = nameof(Break_connector_in_pool) + "_without_keep_alive")] [TestCase(true, TestName = nameof(Break_connector_in_pool) + "_with_keep_alive")] public async Task Break_connector_in_pool(bool keepAlive) @@ -1358,8 +1356,7 @@ await conn.ExecuteNonQueryAsync(@" } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/392")] - [NonParallelizable] - [Platform(Exclude = "MacOsX", Reason = "Flaky in CI on Mac")] + [NonParallelizable] // Drops and creates same database across modes public async Task Non_UTF8_Encoding() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/test/Npgsql.Tests/MultipleHostsTests.cs b/test/Npgsql.Tests/MultipleHostsTests.cs index 9025586c55..a98e0d60c2 100644 --- a/test/Npgsql.Tests/MultipleHostsTests.cs +++ b/test/Npgsql.Tests/MultipleHostsTests.cs @@ -296,7 +296,6 @@ public async Task All_hosts_are_unavailable( } [Test] - [Platform(Exclude = "MacOsX", Reason = "Flaky in CI on Mac")] public async Task First_host_is_down() { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); diff --git a/test/Npgsql.Tests/Replication/CommonLogicalReplicationTests.cs b/test/Npgsql.Tests/Replication/CommonLogicalReplicationTests.cs index a8a363a583..cb434edd8a 100644 --- a/test/Npgsql.Tests/Replication/CommonLogicalReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/CommonLogicalReplicationTests.cs @@ -16,7 +16,6 @@ namespace Npgsql.Tests.Replication; /// for the individual logical replication tests, they are in fact not, because /// the methods they test are extension points for plugin developers. /// -[Platform(Exclude = "MacOsX", Reason = "Replication tests are flaky in CI on Mac")] [NonParallelizable] public class CommonLogicalReplicationTests : SafeReplicationTestBase { diff --git a/test/Npgsql.Tests/Replication/CommonReplicationTests.cs b/test/Npgsql.Tests/Replication/CommonReplicationTests.cs index c57280e184..2be1f3faff 100644 --- a/test/Npgsql.Tests/Replication/CommonReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/CommonReplicationTests.cs @@ -14,7 +14,6 @@ namespace Npgsql.Tests.Replication; [TestFixture(typeof(LogicalReplicationConnection))] [TestFixture(typeof(PhysicalReplicationConnection))] -[Platform(Exclude = "MacOsX", Reason = "Replication tests are flaky in CI on Mac")] [NonParallelizable] public class CommonReplicationTests : SafeReplicationTestBase where TConnection : ReplicationConnection, new() diff --git a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs index 3bbcd1b6ac..a9a90842d3 100644 --- a/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/PgOutputReplicationTests.cs @@ -645,7 +645,6 @@ await c.ExecuteNonQueryAsync(@$" await NextMessage(messages); }, nameof(Dispose_while_replicating)); - [Platform(Exclude = "MacOsX", Reason = "Test is flaky in CI on Mac, see https://github.com/npgsql/npgsql/issues/5294")] [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, false)] diff --git a/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs b/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs index 3ecedfdfdd..406be7b809 100644 --- a/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs +++ b/test/Npgsql.Tests/Replication/TestDecodingReplicationTests.cs @@ -12,7 +12,6 @@ namespace Npgsql.Tests.Replication; /// implementation of logical replication was still somewhat incomplete. /// Please don't change them without confirming that they still work on those old versions. /// -[Platform(Exclude = "MacOsX", Reason = "Replication tests are flaky in CI on Mac")] [NonParallelizable] // These tests aren't designed to be parallelizable public class TestDecodingReplicationTests : SafeReplicationTestBase { From eaa88b87bc36e3ddc4a7de1c524e3e53efcc1c99 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Fri, 21 Nov 2025 00:34:47 +0100 Subject: [PATCH 150/155] Re-enable many NonParallelizable and Ignore tests (#6339) --- test/Npgsql.Tests/ConnectionTests.cs | 8 ++- test/Npgsql.Tests/CopyTests.cs | 3 +- test/Npgsql.Tests/DataAdapterTests.cs | 20 +++--- test/Npgsql.Tests/NpgsqlParameterTests.cs | 72 +++++----------------- test/Npgsql.Tests/TypeMapperTests.cs | 4 +- test/Npgsql.Tests/Types/MultirangeTests.cs | 1 - test/Npgsql.Tests/Types/RangeTests.cs | 1 - test/Npgsql.Tests/Types/TextTests.cs | 1 - 8 files changed, 30 insertions(+), 80 deletions(-) diff --git a/test/Npgsql.Tests/ConnectionTests.cs b/test/Npgsql.Tests/ConnectionTests.cs index 2eed87dab3..106daae81f 100644 --- a/test/Npgsql.Tests/ConnectionTests.cs +++ b/test/Npgsql.Tests/ConnectionTests.cs @@ -188,9 +188,11 @@ public async Task Connection_refused_async(bool pooled) #endif [Test] - [Ignore("Fails in a non-determinstic manner and only on the build server... investigate...")] public void Invalid_Username() { + if (IsMultiplexing) + Assert.Ignore(); + var connString = new NpgsqlConnectionStringBuilder(ConnectionString) { Username = "unknown", Pooling = false @@ -1257,9 +1259,11 @@ public async Task Many_open_close_with_transaction() [Test] [IssueLink("https://github.com/npgsql/npgsql/issues/927")] [IssueLink("https://github.com/npgsql/npgsql/issues/736")] - [Ignore("Fails when running the entire test suite but not on its own...")] public async Task Rollback_on_close() { + if (IsMultiplexing) + Assert.Ignore(); + // Npgsql 3.0.0 to 3.0.4 prepended a rollback for the next time the connector is used, as an optimization. // This caused some issues (#927) and was removed. diff --git a/test/Npgsql.Tests/CopyTests.cs b/test/Npgsql.Tests/CopyTests.cs index 4cddb400eb..eb43420917 100644 --- a/test/Npgsql.Tests/CopyTests.cs +++ b/test/Npgsql.Tests/CopyTests.cs @@ -677,7 +677,6 @@ public async Task Wrong_format_binary_export() } [Test, NonParallelizable, IssueLink("https://github.com/npgsql/npgsql/issues/661")] - [Ignore("Unreliable")] public async Task Unexpected_exception_binary_import() { if (IsMultiplexing) @@ -701,7 +700,7 @@ public async Task Unexpected_exception_binary_import() writer.StartRow(); writer.Write(data); writer.Dispose(); - }, Throws.Exception.TypeOf()); + }, Throws.Exception.InstanceOf()); Assert.That(conn.FullState, Is.EqualTo(ConnectionState.Broken)); } diff --git a/test/Npgsql.Tests/DataAdapterTests.cs b/test/Npgsql.Tests/DataAdapterTests.cs index 3c91521ae1..91de36e734 100644 --- a/test/Npgsql.Tests/DataAdapterTests.cs +++ b/test/Npgsql.Tests/DataAdapterTests.cs @@ -141,7 +141,6 @@ public async Task DataAdapter_update_return_value() } [Test] - [Ignore("")] public async Task DataAdapter_update_return_value2() { using var conn = await OpenConnectionAsync(); @@ -158,15 +157,15 @@ public async Task DataAdapter_update_return_value2() da.Update(ds); //## change id from 1 to 2 - cmd.CommandText = $"update {table} set field_float4 = 0.8"; + cmd.CommandText = $"update {table} set field_numeric = 0.8"; cmd.ExecuteNonQuery(); //## change value to newvalue ds.Tables[0].Rows[0][1] = 0.7; //## update should fail, and make a DBConcurrencyException var count = da.Update(ds); - //## count is 1, even if the isn't updated in the database - Assert.That(count, Is.EqualTo(0)); + //## count is 1, even if the row isn't updated in the database + Assert.That(count, Is.EqualTo(1)); } [Test] @@ -189,7 +188,6 @@ public async Task Fill_with_empty_resultset() } [Test] - [Ignore("")] public async Task Fill_add_with_key() { using var conn = await OpenConnectionAsync(); @@ -211,7 +209,7 @@ public async Task Fill_add_with_key() Assert.That(field_serial.ColumnName, Is.EqualTo("field_serial")); Assert.That(field_serial.DataType, Is.EqualTo(typeof(int))); Assert.That(field_serial.Ordinal, Is.EqualTo(0)); - Assert.That(field_serial.Unique); + Assert.That(field_serial.Unique, Is.False); Assert.That(field_int2.AllowDBNull); Assert.That(field_int2.AutoIncrement, Is.False); @@ -329,7 +327,6 @@ public async Task Fill_with_duplicate_column_name() } [Test] - [Ignore("")] public Task Update_with_DataSet() => DoUpdateWithDataSet(); public async Task DoUpdateWithDataSet() @@ -365,7 +362,6 @@ public async Task DoUpdateWithDataSet() } [Test] - [Ignore("")] public async Task Insert_with_CommandBuilder_case_sensitive() { using var conn = await OpenConnectionAsync(); @@ -380,7 +376,7 @@ public async Task Insert_with_CommandBuilder_case_sensitive() var dt = ds.Tables[0]; var dr = dt.NewRow(); - dr["Field_Case_Sensitive"] = 4; + dr["Field_int4"] = 4; dt.Rows.Add(dr); var ds2 = ds.GetChanges()!; @@ -390,7 +386,7 @@ public async Task Insert_with_CommandBuilder_case_sensitive() using var dr2 = new NpgsqlCommand($"select * from {table}", conn).ExecuteReader(); dr2.Read(); - Assert.That(dr2[1], Is.EqualTo(4)); + Assert.That(dr2["field_int4"], Is.EqualTo(4)); } [Test] @@ -454,7 +450,6 @@ public async Task DataAdapter_command_access() [Test, Description("Makes sure that the INSERT/UPDATE/DELETE commands are auto-populated on NpgsqlDataAdapter")] [IssueLink("https://github.com/npgsql/npgsql/issues/179")] - [Ignore("Somehow related to us using a temporary table???")] public async Task Auto_populate_adapter_commands() { using var conn = await OpenConnectionAsync(); @@ -494,7 +489,6 @@ public void Command_builder_quoting() [Test, Description("Makes sure a correct SQL string is built with GetUpdateCommand(true) using correct parameter names and placeholders")] [IssueLink("https://github.com/npgsql/npgsql/issues/397")] - [Ignore("Somehow related to us using a temporary table???")] public async Task Get_UpdateCommand() { using var conn = await OpenConnectionAsync(); @@ -538,7 +532,7 @@ public async Task Load_DataTable() public Task SetupTempTable(NpgsqlConnection conn) => CreateTempTable(conn, @" -field_pk SERIAL PRIMARY KEY, +field_pk INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, field_serial SERIAL, field_int2 SMALLINT, field_int4 INTEGER, diff --git a/test/Npgsql.Tests/NpgsqlParameterTests.cs b/test/Npgsql.Tests/NpgsqlParameterTests.cs index e1f0ef9c48..6070cc7266 100644 --- a/test/Npgsql.Tests/NpgsqlParameterTests.cs +++ b/test/Npgsql.Tests/NpgsqlParameterTests.cs @@ -4,7 +4,6 @@ using System.Data; using System.Data.Common; using System.Threading.Tasks; -using Npgsql.Internal.Postgres; namespace Npgsql.Tests; @@ -326,44 +325,6 @@ public void Clone_generic() #endregion - [Test] - [Ignore("")] - public void InferType_invalid_throws() - { - var notsupported = new object[] - { - ushort.MaxValue, - uint.MaxValue, - ulong.MaxValue, - sbyte.MaxValue, - new NpgsqlParameter() - }; - - var param = new NpgsqlParameter(); - - for (var i = 0; i < notsupported.Length; i++) - { - try - { - param.Value = notsupported[i]; - Assert.Fail("#A1:" + i); - } - catch (FormatException) - { - // appears to be bug in .NET 1.1 while - // constructing exception message - } - catch (ArgumentException ex) - { - // The parameter data type of ... is invalid - Assert.That(ex.GetType(), Is.EqualTo(typeof(ArgumentException)), "#A2"); - Assert.That(ex.InnerException, Is.Null, "#A3"); - Assert.That(ex.Message, Is.Not.Null, "#A4"); - Assert.That(ex.ParamName, Is.Null, "#A5"); - } - } - } - [Test] // bug #320196 public void Parameter_null() { @@ -379,50 +340,49 @@ public void Parameter_null() } [Test] - [Ignore("")] public void Parameter_type() { NpgsqlParameter p; // If Type is not set, then type is inferred from the value // assigned. The Type should be inferred everytime Value is assigned - // If value is null or DBNull, then the current Type should be reset to Text. - p = new NpgsqlParameter(); + // If value is null or DBNull, then the current Type should be reset to Unknown (DbType.Object and NpgsqlDbType.Unknown). + p = new NpgsqlParameter { Value = "" }; Assert.That(p.DbType, Is.EqualTo(DbType.String), "#A1"); Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#A2"); p.Value = DBNull.Value; - Assert.That(p.DbType, Is.EqualTo(DbType.String), "#B1"); - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#B2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#B1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#B2"); p.Value = 1; Assert.That(p.DbType, Is.EqualTo(DbType.Int32), "#C1"); Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Integer), "#C2"); p.Value = DBNull.Value; - Assert.That(p.DbType, Is.EqualTo(DbType.String), "#D1"); - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#D2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#D1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#D2"); p.Value = new byte[] { 0x0a }; Assert.That(p.DbType, Is.EqualTo(DbType.Binary), "#E1"); Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Bytea), "#E2"); p.Value = null; - Assert.That(p.DbType, Is.EqualTo(DbType.String), "#F1"); - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#F2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#F1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#F2"); p.Value = DateTime.Now; - Assert.That(p.DbType, Is.EqualTo(DbType.DateTime), "#G1"); + Assert.That(p.DbType, Is.EqualTo(DbType.DateTime2), "#G1"); Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#G2"); p.Value = null; - Assert.That(p.DbType, Is.EqualTo(DbType.String), "#H1"); - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Text), "#H2"); + Assert.That(p.DbType, Is.EqualTo(DbType.Object), "#H1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown), "#H2"); // If DbType is set, then the NpgsqlDbType should not be // inferred from the value assigned. p = new NpgsqlParameter(); p.DbType = DbType.DateTime; - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I1"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#I1"); p.Value = 1; - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I2"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#I2"); p.Value = null; - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I3"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#I3"); p.Value = DBNull.Value; - Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp), "#I4"); + Assert.That(p.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz), "#I4"); // If NpgsqlDbType is set, then the DbType should not be // inferred from the value assigned. @@ -447,7 +407,6 @@ public async Task Match_param_index_case_insensitively() } [Test] - [Ignore("")] public void ParameterName() { var p = new NpgsqlParameter(); @@ -540,7 +499,6 @@ public void ParameterName_retains_prefix() => Assert.That(new NpgsqlParameter("@p", DbType.String).ParameterName, Is.EqualTo("@p")); [Test] - [Ignore("")] public void SourceColumn() { var p = new NpgsqlParameter(); diff --git a/test/Npgsql.Tests/TypeMapperTests.cs b/test/Npgsql.Tests/TypeMapperTests.cs index 2819c80810..469a57be01 100644 --- a/test/Npgsql.Tests/TypeMapperTests.cs +++ b/test/Npgsql.Tests/TypeMapperTests.cs @@ -45,7 +45,6 @@ public async Task ReloadTypes_across_connections_in_data_source() } [Test] - [NonParallelizable] // Depends on citext which could be dropped concurrently public async Task String_to_citext() { await using var adminConnection = await OpenConnectionAsync(); @@ -62,7 +61,6 @@ public async Task String_to_citext() } [Test] - [NonParallelizable] // Depends on citext which could be dropped concurrently public async Task String_to_citext_with_db_type_string() { await using var adminConnection = await OpenConnectionAsync(); @@ -152,7 +150,7 @@ await connection.ExecuteNonQueryAsync($""" } [Test, IssueLink("https://github.com/npgsql/npgsql/issues/4582")] - [NonParallelizable] // Drops extension + [NonParallelizable] // Drops global citext extension. public async Task Type_in_non_default_schema() { await using var conn = await OpenConnectionAsync(); diff --git a/test/Npgsql.Tests/Types/MultirangeTests.cs b/test/Npgsql.Tests/Types/MultirangeTests.cs index adb56ed2b9..1de001f93b 100644 --- a/test/Npgsql.Tests/Types/MultirangeTests.cs +++ b/test/Npgsql.Tests/Types/MultirangeTests.cs @@ -100,7 +100,6 @@ public Task Multirange_as_list( sqlLiteral, pgTypeName, npgsqlDbType, isDefaultForReading: false, isDefaultForWriting: isDefaultForWriting); [Test] - [NonParallelizable] public async Task Unmapped_multirange_with_mapped_subtype() { await using var dataSource = CreateDataSource(b => b.EnableUnmappedTypes().ConnectionStringBuilder.MaxPoolSize = 1); diff --git a/test/Npgsql.Tests/Types/RangeTests.cs b/test/Npgsql.Tests/Types/RangeTests.cs index 0b90824d95..7582d4ad40 100644 --- a/test/Npgsql.Tests/Types/RangeTests.cs +++ b/test/Npgsql.Tests/Types/RangeTests.cs @@ -165,7 +165,6 @@ public async Task TimestampTz_range_with_DateTimeOffset() } [Test] - [NonParallelizable] public async Task Unmapped_range_with_mapped_subtype() { await using var dataSource = CreateDataSource(b => b.EnableUnmappedTypes().ConnectionStringBuilder.MaxPoolSize = 1); diff --git a/test/Npgsql.Tests/Types/TextTests.cs b/test/Npgsql.Tests/Types/TextTests.cs index 13dd94861b..6d4adae741 100644 --- a/test/Npgsql.Tests/Types/TextTests.cs +++ b/test/Npgsql.Tests/Types/TextTests.cs @@ -44,7 +44,6 @@ public Task Char_as_char() => AssertType('f', "f", "character", NpgsqlDbType.Char, inferredDbType: DbType.String, isDefault: false); [Test] - [NonParallelizable] public async Task Citext_as_string() { await using var conn = await OpenConnectionAsync(); From 4f4fe1990b7922e9288ee6cc75a9c53c4d1f3ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Sat, 22 Nov 2025 09:45:29 +0100 Subject: [PATCH 151/155] Instrument NpgsqlBinaryImporter with OpenTelemetry (#5921) Co-authored-by: Shay Rojansky --- src/Npgsql/Internal/NpgsqlConnector.cs | 21 + src/Npgsql/NpgsqlActivitySource.cs | 25 + src/Npgsql/NpgsqlBinaryExporter.cs | 159 +++-- src/Npgsql/NpgsqlBinaryImporter.cs | 111 ++- src/Npgsql/NpgsqlConnection.cs | 6 +- src/Npgsql/NpgsqlRawCopyStream.cs | 167 +++-- src/Npgsql/NpgsqlTracingOptionsBuilder.cs | 41 +- src/Npgsql/PublicAPI.Unshipped.txt | 3 + test/Npgsql.Tests/TracingTests.cs | 785 ++++++++++++++++++---- 9 files changed, 1041 insertions(+), 277 deletions(-) diff --git a/src/Npgsql/Internal/NpgsqlConnector.cs b/src/Npgsql/Internal/NpgsqlConnector.cs index a90da65959..617ddcd03e 100644 --- a/src/Npgsql/Internal/NpgsqlConnector.cs +++ b/src/Npgsql/Internal/NpgsqlConnector.cs @@ -3204,6 +3204,27 @@ void ReadParameterStatus(ReadOnlySpan incomingName, ReadOnlySpan inc return null; } + internal Activity? TraceCopyStart(string copyCommand, string operation) + { + Activity? activity = null; + if (NpgsqlActivitySource.IsEnabled) + { + var tracingOptions = DataSource.Configuration.TracingOptions; + + if (tracingOptions.CopyOperationFilter?.Invoke(copyCommand) ?? true) + { + var spanName = tracingOptions.CopyOperationSpanNameProvider?.Invoke(copyCommand); + activity = NpgsqlActivitySource.CopyStart(copyCommand, this, spanName, operation); + + if (activity != null) + { + tracingOptions.CopyOperationEnrichmentCallback?.Invoke(activity, copyCommand); + } + } + } + return activity; + } + #endregion Misc } diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index 8c41cbfca1..91da0f0548 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -153,6 +153,31 @@ internal static void SetException(Activity activity, Exception exception, bool e activity.Dispose(); } + internal static Activity? CopyStart(string command, NpgsqlConnector connector, string? spanName, string operation) + { + var activity = Source.StartActivity(spanName ?? operation, ActivityKind.Client); + if (activity is not { IsAllDataRequested: true }) + return activity; + activity.SetTag("db.query.text", command); + activity.SetTag("db.operation.name", operation); + Enrich(activity, connector); + return activity; + } + + internal static void SetOperation(Activity activity, string operation) + { + if (!activity.IsAllDataRequested) + return; + activity.SetTag("db.operation.name", operation); + } + + internal static void CopyStop(Activity activity, ulong? rows = null) + { + if (rows.HasValue) + activity.SetTag("db.npgsql.rows", rows.Value); + activity.Dispose(); + } + static string GetLibraryVersion() => typeof(NpgsqlDataSource).Assembly .GetCustomAttribute()? diff --git a/src/Npgsql/NpgsqlBinaryExporter.cs b/src/Npgsql/NpgsqlBinaryExporter.cs index 9473c95959..4828f0ecb1 100644 --- a/src/Npgsql/NpgsqlBinaryExporter.cs +++ b/src/Npgsql/NpgsqlBinaryExporter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -49,6 +49,8 @@ public TimeSpan Timeout set => _buf.Timeout = value; } + Activity? _activity; + #endregion #region Construction / Initialization @@ -64,39 +66,50 @@ internal NpgsqlBinaryExporter(NpgsqlConnector connector) internal async Task Init(string copyToCommand, bool async, CancellationToken cancellationToken = default) { - await _connector.WriteQuery(copyToCommand, async, cancellationToken).ConfigureAwait(false); - await _connector.Flush(async, cancellationToken).ConfigureAwait(false); - - using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); + Debug.Assert(_activity is null); + _activity = _connector.TraceCopyStart(copyToCommand, "COPY TO"); - CopyOutResponseMessage copyOutResponse; - var msg = await _connector.ReadMessage(async).ConfigureAwait(false); - switch (msg.Code) + try { - case BackendMessageCode.CopyOutResponse: - copyOutResponse = (CopyOutResponseMessage)msg; - if (!copyOutResponse.IsBinary) + await _connector.WriteQuery(copyToCommand, async, cancellationToken).ConfigureAwait(false); + await _connector.Flush(async, cancellationToken).ConfigureAwait(false); + + using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); + + CopyOutResponseMessage copyOutResponse; + var msg = await _connector.ReadMessage(async).ConfigureAwait(false); + switch (msg.Code) { - throw _connector.Break( - new ArgumentException("copyToCommand triggered a text transfer, only binary is allowed", - nameof(copyToCommand))); + case BackendMessageCode.CopyOutResponse: + copyOutResponse = (CopyOutResponseMessage)msg; + if (!copyOutResponse.IsBinary) + { + throw _connector.Break( + new ArgumentException("copyToCommand triggered a text transfer, only binary is allowed", + nameof(copyToCommand))); + } + break; + case BackendMessageCode.CommandComplete: + throw new InvalidOperationException( + "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + + "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + + "Note that your data has been successfully imported/exported."); + default: + throw _connector.UnexpectedMessageReceived(msg.Code); } - break; - case BackendMessageCode.CommandComplete: - throw new InvalidOperationException( - "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + - "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + - "Note that your data has been successfully imported/exported."); - default: - throw _connector.UnexpectedMessageReceived(msg.Code); - } - _state = ExporterState.Ready; - NumColumns = copyOutResponse.NumColumns; - _columnInfoCache = new PgConverterInfo[NumColumns]; - _rowsExported = 0; - _endOfMessagePos = _buf.CumulativeReadPosition; - await ReadHeader(async).ConfigureAwait(false); + _state = ExporterState.Ready; + NumColumns = copyOutResponse.NumColumns; + _columnInfoCache = new PgConverterInfo[NumColumns]; + _rowsExported = 0; + _endOfMessagePos = _buf.CumulativeReadPosition; + await ReadHeader(async).ConfigureAwait(false); + } + catch (Exception e) + { + TraceSetException(e); + throw; + } } async Task ReadHeader(bool async) @@ -476,40 +489,50 @@ async ValueTask DisposeAsync(bool async) if (_state == ExporterState.Disposed) return; - if (_state is ExporterState.Consumed or ExporterState.Uninitialized) - { - LogMessages.BinaryCopyOperationCompleted(_copyLogger, _rowsExported, _connector.Id); - } - else if (!_connector.IsBroken) + try { - try - { - using var registration = _connector.StartNestedCancellableOperation(attemptPgCancellation: false); - // Be sure to commit the reader. - if (async) - await PgReader.CommitAsync().ConfigureAwait(false); - else - PgReader.Commit(); - // Finish the current CopyData message - await _buf.Skip(async, checked((int)(_endOfMessagePos - _buf.CumulativeReadPosition))).ConfigureAwait(false); - // Read to the end - _connector.SkipUntil(BackendMessageCode.CopyDone); - // We intentionally do not pass a CancellationToken since we don't want to cancel cleanup - Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); - Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); - } - catch (OperationCanceledException e) when (e.InnerException is PostgresException { SqlState: PostgresErrorCodes.QueryCanceled }) + if (_state is ExporterState.Consumed or ExporterState.Uninitialized) { - LogMessages.CopyOperationCancelled(_copyLogger, _connector.Id); + LogMessages.BinaryCopyOperationCompleted(_copyLogger, _rowsExported, _connector.Id); + TraceExportStop(); } - catch (Exception e) + else if (!_connector.IsBroken) { - LogMessages.ExceptionWhenDisposingCopyOperation(_copyLogger, _connector.Id, e); + try + { + using var registration = _connector.StartNestedCancellableOperation(attemptPgCancellation: false); + // Be sure to commit the reader. + if (async) + await PgReader.CommitAsync().ConfigureAwait(false); + else + PgReader.Commit(); + // Finish the current CopyData message + await _buf.Skip(async, checked((int)(_endOfMessagePos - _buf.CumulativeReadPosition))).ConfigureAwait(false); + // Read to the end + _connector.SkipUntil(BackendMessageCode.CopyDone); + // We intentionally do not pass a CancellationToken since we don't want to cancel cleanup + Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); + Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); + + TraceExportStop(); + } + catch (OperationCanceledException e) when (e.InnerException is PostgresException { SqlState: PostgresErrorCodes.QueryCanceled }) + { + LogMessages.CopyOperationCancelled(_copyLogger, _connector.Id); + TraceExportStop(); + } + catch (Exception e) + { + LogMessages.ExceptionWhenDisposingCopyOperation(_copyLogger, _connector.Id, e); + TraceSetException(e); + } } } - - _connector.EndUserAction(); - Cleanup(); + finally + { + _connector.EndUserAction(); + Cleanup(); + } void Cleanup() { @@ -530,6 +553,28 @@ void Cleanup() #endregion + #region Tracing + + void TraceExportStop() + { + if (_activity is not null) + { + NpgsqlActivitySource.CopyStop(_activity, _rowsExported); + _activity = null; + } + } + + void TraceSetException(Exception exception) + { + if (_activity is not null) + { + NpgsqlActivitySource.SetException(_activity, exception); + _activity = null; + } + } + + #endregion Tracing + #region Enums enum ExporterState diff --git a/src/Npgsql/NpgsqlBinaryImporter.cs b/src/Npgsql/NpgsqlBinaryImporter.cs index 8c240468d4..6cd592dd06 100644 --- a/src/Npgsql/NpgsqlBinaryImporter.cs +++ b/src/Npgsql/NpgsqlBinaryImporter.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -45,6 +46,8 @@ public sealed class NpgsqlBinaryImporter : ICancelable readonly ILogger _copyLogger; PgWriter _pgWriter = null!; // Setup in Init + Activity? _activity; + /// /// Current timeout /// @@ -72,40 +75,51 @@ internal NpgsqlBinaryImporter(NpgsqlConnector connector) internal async Task Init(string copyFromCommand, bool async, CancellationToken cancellationToken = default) { - await _connector.WriteQuery(copyFromCommand, async, cancellationToken).ConfigureAwait(false); - await _connector.Flush(async, cancellationToken).ConfigureAwait(false); + Debug.Assert(_activity is null); + _activity = _connector.TraceCopyStart(copyFromCommand, "COPY FROM"); - using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); - - CopyInResponseMessage copyInResponse; - var msg = await _connector.ReadMessage(async).ConfigureAwait(false); - switch (msg.Code) + try { - case BackendMessageCode.CopyInResponse: - copyInResponse = (CopyInResponseMessage)msg; - if (!copyInResponse.IsBinary) + await _connector.WriteQuery(copyFromCommand, async, cancellationToken).ConfigureAwait(false); + await _connector.Flush(async, cancellationToken).ConfigureAwait(false); + + using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); + + CopyInResponseMessage copyInResponse; + var msg = await _connector.ReadMessage(async).ConfigureAwait(false); + switch (msg.Code) { - throw _connector.Break( - new ArgumentException("copyFromCommand triggered a text transfer, only binary is allowed", - nameof(copyFromCommand))); + case BackendMessageCode.CopyInResponse: + copyInResponse = (CopyInResponseMessage)msg; + if (!copyInResponse.IsBinary) + { + throw _connector.Break( + new ArgumentException("copyFromCommand triggered a text transfer, only binary is allowed", + nameof(copyFromCommand))); + } + break; + case BackendMessageCode.CommandComplete: + throw new InvalidOperationException( + "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + + "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + + "Note that your data has been successfully imported/exported."); + default: + throw _connector.UnexpectedMessageReceived(msg.Code); } - break; - case BackendMessageCode.CommandComplete: - throw new InvalidOperationException( - "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + - "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + - "Note that your data has been successfully imported/exported."); - default: - throw _connector.UnexpectedMessageReceived(msg.Code); - } - _state = ImporterState.Ready; - _params = new NpgsqlParameter[copyInResponse.NumColumns]; - _rowsImported = 0; - _buf.StartCopyMode(); - WriteHeader(); - // Only init after header. - _pgWriter = _buf.GetWriter(_connector.DatabaseInfo); + _state = ImporterState.Ready; + _params = new NpgsqlParameter[copyInResponse.NumColumns]; + _rowsImported = 0; + _buf.StartCopyMode(); + WriteHeader(); + // Only init after header. + _pgWriter = _buf.GetWriter(_connector.DatabaseInfo); + } + catch (Exception e) + { + TraceSetException(e); + throw; + } } void WriteHeader() @@ -308,6 +322,7 @@ await param.Write(async, _pgWriter.WithFlushMode(async ? FlushMode.NonBlocking : } catch (Exception ex) { + TraceSetException(ex); _connector.Break(ex); throw; } @@ -426,8 +441,9 @@ async ValueTask Complete(bool async, CancellationToken cancellationToken _state = ImporterState.Committed; return cmdComplete.Rows; } - catch + catch (Exception e) { + TraceSetException(e); Cleanup(); throw; } @@ -521,6 +537,7 @@ async ValueTask CloseAsync(bool async, CancellationToken cancellationToken = def throw new Exception("Invalid state: " + _state); } + TraceImportStop(); Cleanup(); } @@ -581,4 +598,38 @@ enum ImporterState void ThrowColumnMismatch() => throw new InvalidOperationException($"The binary import operation was started with {NumColumns} column(s), but {_column + 1} value(s) were provided."); + + #region Tracing + + void TraceImportStop() + { + if (_activity is not null) + { + switch (_state) + { + case ImporterState.Committed: + NpgsqlActivitySource.CopyStop(_activity, _rowsImported); + break; + case ImporterState.Cancelled: + NpgsqlActivitySource.CopyStop(_activity, rows: 0); + break; + default: + Debug.Fail("Invalid state: " + _state); + break; + } + + _activity = null; + } + } + + void TraceSetException(Exception exception) + { + if (_activity is not null) + { + NpgsqlActivitySource.SetException(_activity, exception); + _activity = null; + } + } + + #endregion Tracing } diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index ca8b6ba9fe..57299a3eec 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -1274,7 +1274,7 @@ async Task BeginTextImport(bool async, string copyFromComm var copyStream = new NpgsqlRawCopyStream(connector); try { - await copyStream.Init(copyFromCommand, async, cancellationToken).ConfigureAwait(false); + await copyStream.Init(copyFromCommand, async, forExport: false, cancellationToken).ConfigureAwait(false); var writer = new NpgsqlCopyTextWriter(connector, copyStream); connector.CurrentCopyOperation = writer; return writer; @@ -1340,7 +1340,7 @@ async Task BeginTextExport(bool async, string copyToComman var copyStream = new NpgsqlRawCopyStream(connector); try { - await copyStream.Init(copyToCommand, async, cancellationToken).ConfigureAwait(false); + await copyStream.Init(copyToCommand, async, forExport: true, cancellationToken).ConfigureAwait(false); var reader = new NpgsqlCopyTextReader(connector, copyStream); connector.CurrentCopyOperation = reader; return reader; @@ -1406,7 +1406,7 @@ async Task BeginRawBinaryCopy(bool async, string copyComman var stream = new NpgsqlRawCopyStream(connector); try { - await stream.Init(copyCommand, async, cancellationToken).ConfigureAwait(false); + await stream.Init(copyCommand, async, forExport: null, cancellationToken).ConfigureAwait(false); if (!stream.IsBinary) { // TODO: Stop the COPY operation gracefully, no breaking diff --git a/src/Npgsql/NpgsqlRawCopyStream.cs b/src/Npgsql/NpgsqlRawCopyStream.cs index d0cfad82a7..45cfdf825e 100644 --- a/src/Npgsql/NpgsqlRawCopyStream.cs +++ b/src/Npgsql/NpgsqlRawCopyStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading; @@ -60,6 +60,7 @@ public override int ReadTimeout ]; readonly ILogger _copyLogger; + Activity? _activity; #endregion @@ -73,36 +74,54 @@ internal NpgsqlRawCopyStream(NpgsqlConnector connector) _copyLogger = connector.LoggingConfiguration.CopyLogger; } - internal async Task Init(string copyCommand, bool async, CancellationToken cancellationToken = default) + internal async Task Init(string copyCommand, bool async, bool? forExport, CancellationToken cancellationToken = default) { - await _connector.WriteQuery(copyCommand, async, cancellationToken).ConfigureAwait(false); - await _connector.Flush(async, cancellationToken).ConfigureAwait(false); + Debug.Assert(_activity is null); + _activity = _connector.TraceCopyStart(copyCommand, forExport switch + { + true => "COPY TO", + false => "COPY FROM", + null => "COPY", + }); - using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); + try + { + await _connector.WriteQuery(copyCommand, async, cancellationToken).ConfigureAwait(false); + await _connector.Flush(async, cancellationToken).ConfigureAwait(false); - var msg = await _connector.ReadMessage(async).ConfigureAwait(false); - switch (msg.Code) + using var registration = _connector.StartNestedCancellableOperation(cancellationToken, attemptPgCancellation: false); + + var msg = await _connector.ReadMessage(async).ConfigureAwait(false); + switch (msg.Code) + { + case BackendMessageCode.CopyInResponse: + _state = CopyStreamState.Ready; + var copyInResponse = (CopyInResponseMessage)msg; + IsBinary = copyInResponse.IsBinary; + _canWrite = true; + _writeBuf.StartCopyMode(); + TraceSetImport(); + break; + case BackendMessageCode.CopyOutResponse: + _state = CopyStreamState.Ready; + var copyOutResponse = (CopyOutResponseMessage)msg; + IsBinary = copyOutResponse.IsBinary; + _canRead = true; + TraceSetExport(); + break; + case BackendMessageCode.CommandComplete: + throw new InvalidOperationException( + "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + + "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + + "Note that your data has been successfully imported/exported."); + default: + throw _connector.UnexpectedMessageReceived(msg.Code); + } + } + catch (Exception e) { - case BackendMessageCode.CopyInResponse: - _state = CopyStreamState.Ready; - var copyInResponse = (CopyInResponseMessage) msg; - IsBinary = copyInResponse.IsBinary; - _canWrite = true; - _writeBuf.StartCopyMode(); - break; - case BackendMessageCode.CopyOutResponse: - _state = CopyStreamState.Ready; - var copyOutResponse = (CopyOutResponseMessage) msg; - IsBinary = copyOutResponse.IsBinary; - _canRead = true; - break; - case BackendMessageCode.CommandComplete: - throw new InvalidOperationException( - "This API only supports import/export from the client, i.e. COPY commands containing TO/FROM STDIN. " + - "To import/export with files on your PostgreSQL machine, simply execute the command with ExecuteNonQuery. " + - "Note that your data has been successfully imported/exported."); - default: - throw _connector.UnexpectedMessageReceived(msg.Code); + TraceSetException(e); + throw; } } @@ -261,10 +280,13 @@ async ValueTask ReadCore(int count, bool async, CancellationToken cancellat // read the next message msg = await _connector.ReadMessage(async).ConfigureAwait(false); } - catch + catch (Exception e) { if (_state != CopyStreamState.Disposed) + { + TraceSetException(e); Cleanup(); + } throw; } @@ -339,7 +361,12 @@ async Task Cancel(bool async) Cleanup(); if (e.SqlState != PostgresErrorCodes.QueryCanceled) + { + TraceSetException(e); throw; + } + + TraceStop(); } } else @@ -357,7 +384,6 @@ async Task Cancel(bool async) public override ValueTask DisposeAsync() => DisposeAsync(disposing: true, async: true); - async ValueTask DisposeAsync(bool disposing, bool async) { if (_state == CopyStreamState.Disposed || !disposing) @@ -369,18 +395,27 @@ async ValueTask DisposeAsync(bool disposing, bool async) if (CanWrite) { - await FlushAsync(async).ConfigureAwait(false); - _writeBuf.EndCopyMode(); - await _connector.WriteCopyDone(async).ConfigureAwait(false); - await _connector.Flush(async).ConfigureAwait(false); - Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); - Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); + try + { + await FlushAsync(async).ConfigureAwait(false); + _writeBuf.EndCopyMode(); + await _connector.WriteCopyDone(async).ConfigureAwait(false); + await _connector.Flush(async).ConfigureAwait(false); + Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); + Expect(await _connector.ReadMessage(async).ConfigureAwait(false), _connector); + TraceStop(); + } + catch (Exception e) + { + TraceSetException(e); + throw; + } } else { - if (_state != CopyStreamState.Consumed && _state != CopyStreamState.Uninitialized) + try { - try + if (_state != CopyStreamState.Consumed && _state != CopyStreamState.Uninitialized) { if (_leftToReadInDataMsg > 0) { @@ -388,14 +423,18 @@ async ValueTask DisposeAsync(bool disposing, bool async) } _connector.SkipUntil(BackendMessageCode.ReadyForQuery); } - catch (OperationCanceledException e) when (e.InnerException is PostgresException { SqlState: PostgresErrorCodes.QueryCanceled }) - { - LogMessages.CopyOperationCancelled(_copyLogger, _connector.Id); - } - catch (Exception e) - { - LogMessages.ExceptionWhenDisposingCopyOperation(_copyLogger, _connector.Id, e); - } + + TraceStop(); + } + catch (OperationCanceledException e) when (e.InnerException is PostgresException { SqlState: PostgresErrorCodes.QueryCanceled }) + { + LogMessages.CopyOperationCancelled(_copyLogger, _connector.Id); + TraceStop(); + } + catch (Exception e) + { + LogMessages.ExceptionWhenDisposingCopyOperation(_copyLogger, _connector.Id, e); + TraceSetException(e); } } } @@ -458,6 +497,44 @@ static void ValidateArguments(byte[] buffer, int offset, int count) } #endregion + #region Tracing + + private void TraceSetImport() + { + if (_activity is not null) + { + NpgsqlActivitySource.SetOperation(_activity, "COPY FROM"); + } + } + + private void TraceSetExport() + { + if (_activity is not null) + { + NpgsqlActivitySource.SetOperation(_activity, "COPY TO"); + } + } + + private void TraceStop() + { + if (_activity is not null) + { + NpgsqlActivitySource.CopyStop(_activity); + _activity = null; + } + } + + private void TraceSetException(Exception e) + { + if (_activity is not null) + { + NpgsqlActivitySource.SetException(_activity, e); + _activity = null; + } + } + + #endregion + #region Enums enum CopyStreamState diff --git a/src/Npgsql/NpgsqlTracingOptionsBuilder.cs b/src/Npgsql/NpgsqlTracingOptionsBuilder.cs index f81f4e7139..d38cf1d32d 100644 --- a/src/Npgsql/NpgsqlTracingOptionsBuilder.cs +++ b/src/Npgsql/NpgsqlTracingOptionsBuilder.cs @@ -17,6 +17,10 @@ public sealed class NpgsqlTracingOptionsBuilder bool _enableFirstResponseEvent = true; bool _enablePhysicalOpenTracing = true; + Func? _copyOperationFilter; + Action? _copyOperationEnrichmentCallback; + Func? _copyOperationSpanNameProvider; + internal NpgsqlTracingOptionsBuilder() { } @@ -99,6 +103,35 @@ public NpgsqlTracingOptionsBuilder EnablePhysicalOpenTracing(bool enable = true) return this; } + /// + /// Configures a filter function that determines whether to emit tracing information for a copy operation. + /// By default, tracing information is emitted for all copy operations. + /// + public NpgsqlTracingOptionsBuilder ConfigureCopyOperationFilter(Func? copyOperationFilter) + { + _copyOperationFilter = copyOperationFilter; + return this; + } + + /// + /// Configures a callback that can enrich the emitted for a given copy operation. + /// + public NpgsqlTracingOptionsBuilder ConfigureCopyOperationEnrichmentCallback(Action? copyOperationEnrichmentCallback) + { + _copyOperationEnrichmentCallback = copyOperationEnrichmentCallback; + return this; + } + + /// + /// Configures a callback that provides the tracing span's name for a copy operation. If null, the default standard + /// span name is used, which is the database name. + /// + public NpgsqlTracingOptionsBuilder ConfigureCopyOperationSpanNameProvider(Func? copyOperationSpanNameProvider) + { + _copyOperationSpanNameProvider = copyOperationSpanNameProvider; + return this; + } + internal NpgsqlTracingOptions Build() => new() { CommandFilter = _commandFilter, @@ -108,7 +141,10 @@ public NpgsqlTracingOptionsBuilder EnablePhysicalOpenTracing(bool enable = true) CommandSpanNameProvider = _commandSpanNameProvider, BatchSpanNameProvider = _batchSpanNameProvider, EnableFirstResponseEvent = _enableFirstResponseEvent, - EnablePhysicalOpenTracing = _enablePhysicalOpenTracing + EnablePhysicalOpenTracing = _enablePhysicalOpenTracing, + CopyOperationFilter = _copyOperationFilter, + CopyOperationEnrichmentCallback = _copyOperationEnrichmentCallback, + CopyOperationSpanNameProvider = _copyOperationSpanNameProvider }; } @@ -122,4 +158,7 @@ sealed class NpgsqlTracingOptions internal Func? BatchSpanNameProvider { get; init; } internal bool EnableFirstResponseEvent { get; init; } internal bool EnablePhysicalOpenTracing { get; init; } + internal Func? CopyOperationFilter { get; init; } + internal Action? CopyOperationEnrichmentCallback { get; init; } + internal Func? CopyOperationSpanNameProvider { get; init; } } diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 9cea3e5609..6694c16f4f 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -59,6 +59,9 @@ Npgsql.NpgsqlTracingOptionsBuilder.ConfigureBatchSpanNameProvider(System.Func? commandEnrichmentCallback) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCommandFilter(System.Func? commandFilter) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCommandSpanNameProvider(System.Func? commandSpanNameProvider) -> Npgsql.NpgsqlTracingOptionsBuilder! +Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCopyOperationEnrichmentCallback(System.Action? copyOperationEnrichmentCallback) -> Npgsql.NpgsqlTracingOptionsBuilder! +Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCopyOperationFilter(System.Func? copyOperationFilter) -> Npgsql.NpgsqlTracingOptionsBuilder! +Npgsql.NpgsqlTracingOptionsBuilder.ConfigureCopyOperationSpanNameProvider(System.Func? copyOperationSpanNameProvider) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.EnableFirstResponseEvent(bool enable = true) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTracingOptionsBuilder.EnablePhysicalOpenTracing(bool enable = true) -> Npgsql.NpgsqlTracingOptionsBuilder! Npgsql.NpgsqlTypeLoadingOptionsBuilder diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index da7f038d4a..3ed31d6a30 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -1,37 +1,34 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using NpgsqlTypes; using NUnit.Framework; +using static Npgsql.Tests.TestUtil; namespace Npgsql.Tests; [NonParallelizable] -public class TracingTests(MultiplexingMode multiplexingMode) : MultiplexingTestBase(multiplexingMode) +[TestFixture(MultiplexingMode.NonMultiplexing, true)] +[TestFixture(MultiplexingMode.NonMultiplexing, false)] +[TestFixture(MultiplexingMode.Multiplexing, true)] +// Sync I/O not supported with multiplexing +public class TracingTests(MultiplexingMode multiplexingMode, bool async) : MultiplexingTestBase(multiplexingMode) { + #region Physical open + [Test] - public async Task Basic_open([Values] bool async) + public async Task PhysicalOpen() { - if (IsMultiplexing && !async) - return; - - var activities = new List(); - - using var activityListener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Npgsql", - Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => activities.Add(activity) - }; - ActivitySource.AddActivityListener(activityListener); - + using var activityListener = StartListener(out var activities); await using var dataSource = CreateDataSource(); - await using var conn = async - ? await dataSource.OpenConnectionAsync() - : dataSource.OpenConnection(); + await using var connection = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); Assert.That(activities, Has.Count.EqualTo(1)); - ValidateActivity(activities[0], conn, IsMultiplexing); + ValidateActivity(activities[0], connection, IsMultiplexing); if (!IsMultiplexing) return; @@ -41,10 +38,10 @@ public async Task Basic_open([Values] bool async) // For multiplexing, we clear the pool to force next query to open another physical connection dataSource.Clear(); - await conn.ExecuteScalarAsync("SELECT 1"); + await connection.ExecuteScalarAsync("SELECT 1"); Assert.That(activities, Has.Count.EqualTo(2)); - ValidateActivity(activities[0], conn, IsMultiplexing); + ValidateActivity(activities[0], connection, IsMultiplexing); // For multiplexing, query's activity can be considered as a parent for physical open's activity Assert.That(activities[0].Parent, Is.SameAs(activities[1])); @@ -75,87 +72,101 @@ static void ValidateActivity(Activity activity, NpgsqlConnection conn, bool isMu } [Test] - public async Task Basic_query([Values] bool async, [Values] bool batch) + public async Task PhysicalOpen_error() { - if (IsMultiplexing && !async) - return; + using var activityListener = StartListener(out var activities); + await using var dataSource = CreateDataSource(x => x.Host = "not-existing-host"); + var exception = Assert.ThrowsAsync(async () => + { + await using var connection = async + ? await dataSource.OpenConnectionAsync() + : dataSource.OpenConnection(); + })!; - var activities = new List(); + var activity = GetSingleActivity(activities, "CONNECT " + dataSource.Settings.Database, "CONNECT " + dataSource.Settings.Database, ActivityStatusCode.Error, exception.Message); - using var activityListener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Npgsql", - Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => activities.Add(activity) - }; - ActivitySource.AddActivityListener(activityListener); + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var exceptionEvent = activity.Events.First(); + Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); + + var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); + Assert.That(exceptionTags, Has.Count.EqualTo(3)); + + Assert.That(exceptionTags["exception.type"], Is.EqualTo(exception.GetType().FullName)); + Assert.That(exceptionTags["exception.message"], Does.Contain(exception.Message)); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain(exception.Message)); + + var activityTags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(activityTags, Has.Count.EqualTo(3)); + + Assert.That(activityTags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(dataSource.ConnectionString)); + + Assert.That(activityTags["error.type"], Is.EqualTo("System.Net.Sockets.SocketException")); + } + [Test] + public async Task PhysicalOpen_disable() + { + using var activityListener = StartListener(out var activities); + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConfigureTracing(options => options.EnablePhysicalOpenTracing(enable: false)); + await using var dataSource = dataSourceBuilder.Build(); + + await using var connection = async ? await dataSource.OpenConnectionAsync() : dataSource.OpenConnection(); + + Assert.That(activities, Is.Empty); + } + + #endregion Physical open + + #region Command execution + + [Test] + public async Task CommandExecute([Values] bool batch) + { var dataSourceBuilder = CreateDataSourceBuilder(); dataSourceBuilder.Name = "TestTracingDataSource"; + dataSourceBuilder.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false)); await using var dataSource = dataSourceBuilder.Build(); - await using var conn = await dataSource.OpenConnectionAsync(); + await using var connection = await dataSource.OpenConnectionAsync(); - // We're not interested in physical open's activity - Assert.That(activities, Has.Count.EqualTo(1)); - activities.Clear(); + using var activityListener = StartListener(out var activities); - await ExecuteScalar(conn, async, batch, "SELECT 42"); + await ExecuteScalar(connection, async, batch, "SELECT 42"); - Assert.That(activities, Has.Count.EqualTo(1)); - var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo("postgresql")); - Assert.That(activity.OperationName, Is.EqualTo("postgresql")); - Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Unset)); + var activity = GetSingleActivity(activities, "postgresql", "postgresql"); Assert.That(activity.Events.Count(), Is.EqualTo(1)); var firstResponseEvent = activity.Events.First(); Assert.That(firstResponseEvent.Name, Is.EqualTo("received-first-response")); var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); - Assert.That(tags, Has.Count.EqualTo(conn.Settings.Port == 5432 ? 6 : 7)); + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 6 : 7)); Assert.That(tags["db.query.text"], Is.EqualTo("SELECT 42")); Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); - Assert.That(tags["db.namespace"], Is.EqualTo(conn.Settings.Database)); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); Assert.That(tags["db.npgsql.data_source"], Is.EqualTo("TestTracingDataSource")); if (IsMultiplexing) Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); else - Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(conn.ProcessID)); + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); } [Test] - public async Task Error_open([Values] bool async) + public async Task CommandExecute_error([Values] bool batch) { - if (IsMultiplexing && !async) - return; + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); - var activities = new List(); - - using var activityListener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Npgsql", - Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => activities.Add(activity) - }; - ActivitySource.AddActivityListener(activityListener); + using var activityListener = StartListener(out var activities); - await using var dataSource = CreateDataSource(x => x.Host = "not-existing-host"); - var ex = Assert.ThrowsAsync(async () => - { - await using var conn = async - ? await dataSource.OpenConnectionAsync() - : dataSource.OpenConnection(); - })!; + Assert.ThrowsAsync(async () => await ExecuteScalar(connection, async, batch, "SELECT * FROM non_existing_table")); - Assert.That(activities, Has.Count.EqualTo(1)); - var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo("CONNECT " + dataSource.Settings.Database)); - Assert.That(activity.OperationName, Is.EqualTo("CONNECT " + dataSource.Settings.Database)); - Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); - Assert.That(activity.StatusDescription, Is.EqualTo(ex.Message)); + var activity = GetSingleActivity(activities, "postgresql", "postgresql", ActivityStatusCode.Error, PostgresErrorCodes.UndefinedTable); Assert.That(activity.Events.Count(), Is.EqualTo(1)); var exceptionEvent = activity.Events.First(); @@ -164,50 +175,283 @@ public async Task Error_open([Values] bool async) var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); Assert.That(exceptionTags, Has.Count.EqualTo(3)); - Assert.That(exceptionTags["exception.type"], Is.EqualTo(ex.GetType().FullName)); - Assert.That(exceptionTags["exception.message"], Does.Contain(ex.Message)); - Assert.That(exceptionTags["exception.stacktrace"], Does.Contain(ex.Message)); + Assert.That(exceptionTags["exception.type"], Is.EqualTo("Npgsql.PostgresException")); + Assert.That(exceptionTags["exception.message"], Does.Contain("relation \"non_existing_table\" does not exist")); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain("relation \"non_existing_table\" does not exist")); var activityTags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); - Assert.That(activityTags, Has.Count.EqualTo(3)); + Assert.That(activityTags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 8 : 9)); + Assert.That(activityTags["db.query.text"], Is.EqualTo("SELECT * FROM non_existing_table")); Assert.That(activityTags["db.system.name"], Is.EqualTo("postgresql")); - Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(dataSource.ConnectionString)); + Assert.That(activityTags["db.namespace"], Is.EqualTo(connection.Settings.Database)); - Assert.That(activityTags["error.type"], Is.EqualTo("System.Net.Sockets.SocketException")); + Assert.That(activityTags["db.response.status_code"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + Assert.That(activityTags["error.type"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + + Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); + + if (IsMultiplexing) + Assert.That(activityTags, Does.ContainKey("db.npgsql.connection_id")); + else + Assert.That(activityTags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); } [Test] - public async Task Error_query([Values] bool async, [Values] bool batch) + public async Task CommandExecute_ConfigureTracing([Values] bool batch) { - if (IsMultiplexing && !async) - return; + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConfigureTracing(options => + { + options + .EnablePhysicalOpenTracing(false) + .EnableFirstResponseEvent(enable: false) + .ConfigureCommandFilter(cmd => cmd.CommandText.Contains('2')) + .ConfigureBatchFilter(batch => batch.BatchCommands[0].CommandText.Contains('2')) + .ConfigureCommandSpanNameProvider(_ => "unknown_query") + .ConfigureBatchSpanNameProvider(_ => "unknown_query") + .ConfigureCommandEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")) + .ConfigureBatchEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")); + }); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + await ExecuteScalar(connection, async, batch, "SELECT 1"); + + Assert.That(activities, Is.Empty); + + await ExecuteScalar(connection, async, batch, "SELECT 2"); + + var activity = GetSingleActivity(activities, "unknown_query", "unknown_query"); + + Assert.That(activity.Events.Count(), Is.EqualTo(0)); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags["custom_tag"], Is.EqualTo("custom_value")); + } + + #endregion Command execution + + #region Binary import + + [Test] + public async Task BinaryImport() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + + using var activityListener = StartListener(out var activities); - var activities = new List(); + var copyFromCommand = $"COPY {table} (field_text, field_int2) FROM STDIN BINARY"; - using var activityListener = new ActivityListener + if (async) { - ShouldListenTo = source => source.Name == "Npgsql", - Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => activities.Add(activity) - }; - ActivitySource.AddActivityListener(activityListener); + await using var writer = await connection.BeginBinaryImportAsync(copyFromCommand); - await using var dataSource = CreateDataSource(); + await writer.StartRowAsync(); + await writer.WriteAsync("Hello"); + await writer.WriteAsync((short)8, NpgsqlDbType.Smallint); + + await writer.CompleteAsync(); + } + else + { + using var writer = connection.BeginBinaryImport(copyFromCommand); + + writer.StartRow(); + writer.Write("Hello"); + writer.Write((short)8, NpgsqlDbType.Smallint); + + writer.Complete(); + } + + var activity = GetSingleActivity(activities, "COPY FROM"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 8 : 9)); + + Assert.That(tags["db.query.text"], Is.EqualTo(copyFromCommand)); + Assert.That(tags["db.operation.name"], Is.EqualTo("COPY FROM")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); + + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); + Assert.That(tags["db.npgsql.rows"], Is.EqualTo(1)); + + if (IsMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); + else + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); + } + + [Test] + public async Task BinaryImport_cancel() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + + using var activityListener = StartListener(out var activities); + + var copyFromCommand = $"COPY {table} (field_text, field_int2) FROM STDIN BINARY"; + + if (async) + { + await using var writer = await connection.BeginBinaryImportAsync(copyFromCommand); + await writer.StartRowAsync(); + await writer.WriteAsync("Hello"); + await writer.WriteAsync((short)8, NpgsqlDbType.Smallint); + // No Complete() call - disposing cancels + } + else + { + using var writer = connection.BeginBinaryImport(copyFromCommand); + writer.StartRow(); + writer.Write("Hello"); + writer.Write((short)8, NpgsqlDbType.Smallint); + // No Complete() call - disposing cancels + } + + _ = GetSingleActivity(activities, "COPY FROM"); + } + + [Test] + public async Task BinaryImport_error() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + var copyFromCommand = $"COPY non_existing_table (field_text, field_int2) FROM STDIN BINARY"; + + var ex = Assert.ThrowsAsync(async () => + { + await using var writer = async + ? await connection.BeginBinaryImportAsync(copyFromCommand) + : connection.BeginBinaryImport(copyFromCommand); + }); + + var activity = GetSingleActivity(activities, "COPY FROM", "COPY FROM", ActivityStatusCode.Error, PostgresErrorCodes.UndefinedTable); + + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var exceptionEvent = activity.Events.First(); + Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); + + var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); + Assert.That(exceptionTags, Has.Count.EqualTo(3)); + + Assert.That(exceptionTags["exception.type"], Is.EqualTo("Npgsql.PostgresException")); + Assert.That(exceptionTags["exception.message"], Does.Contain("relation \"non_existing_table\" does not exist")); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain("relation \"non_existing_table\" does not exist")); + } + + #endregion Binary import + + #region Binary export + + [Test] + public async Task BinaryExport() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + await connection.ExecuteNonQueryAsync($"INSERT INTO {table} (field_text, field_int2) VALUES ('Hello', 8)"); + + using var activityListener = StartListener(out var activities); + + var copyToCommand = $"COPY {table} (field_text, field_int2) TO STDOUT BINARY"; + + if (async) + { + await using var reader = await connection.BeginBinaryExportAsync(copyToCommand); + while (await reader.StartRowAsync() != -1) + { + _ = await reader.ReadAsync(); + _ = await reader.ReadAsync(); + } + } + else + { + using var reader = connection.BeginBinaryExport(copyToCommand); + while (reader.StartRow() != -1) + { + _ = reader.Read(); + _ = reader.Read(); + } + } + + var activity = GetSingleActivity(activities, "COPY TO"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 8 : 9)); + + Assert.That(tags["db.query.text"], Is.EqualTo(copyToCommand)); + Assert.That(tags["db.operation.name"], Is.EqualTo("COPY TO")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); + + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); + Assert.That(tags["db.npgsql.rows"], Is.EqualTo(1)); + + if (IsMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); + else + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); + } + + [Test] + public async Task BinaryExport_cancel() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); await using var conn = await dataSource.OpenConnectionAsync(); - // We're not interested in physical open's activity - Assert.That(activities.Count, Is.EqualTo(1)); - activities.Clear(); + using var activityListener = StartListener(out var activities); - Assert.ThrowsAsync(async () => await ExecuteScalar(conn, async, batch, "SELECT * FROM non_existing_table")); + // This must be large enough to cause Postgres to queue up CopyData messages. + const string copyToCommand = "COPY (select md5(random()::text) as id from generate_series(1, 100000)) TO STDOUT BINARY"; - Assert.That(activities, Has.Count.EqualTo(1)); - var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo("postgresql")); - Assert.That(activity.OperationName, Is.EqualTo("postgresql")); - Assert.That(activity.Status, Is.EqualTo(ActivityStatusCode.Error)); - Assert.That(activity.StatusDescription, Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + if (async) + { + await using var exporter = await conn.BeginBinaryExportAsync(copyToCommand); + await exporter.StartRowAsync(); + await exporter.ReadAsync(); + await exporter.CancelAsync(); + } + else + { + using var exporter = await conn.BeginBinaryExportAsync(copyToCommand); + exporter.StartRow(); + exporter.Read(); + exporter.Cancel(); + } + + _ = GetSingleActivity(activities, "COPY TO"); + } + + [Test] + public async Task BinaryExport_error() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + var copyToCommand = $"COPY non_existing_table (field_text, field_int2) TO STDOUT BINARY"; + var ex = Assert.ThrowsAsync(async () => + { + await using var reader = async + ? await connection.BeginBinaryExportAsync(copyToCommand) + : connection.BeginBinaryExport(copyToCommand); + }); + + var activity = GetSingleActivity(activities, "COPY TO", "COPY TO", ActivityStatusCode.Error, PostgresErrorCodes.UndefinedTable); Assert.That(activity.Events.Count(), Is.EqualTo(1)); var exceptionEvent = activity.Events.First(); @@ -219,78 +463,337 @@ public async Task Error_query([Values] bool async, [Values] bool batch) Assert.That(exceptionTags["exception.type"], Is.EqualTo("Npgsql.PostgresException")); Assert.That(exceptionTags["exception.message"], Does.Contain("relation \"non_existing_table\" does not exist")); Assert.That(exceptionTags["exception.stacktrace"], Does.Contain("relation \"non_existing_table\" does not exist")); + } - var activityTags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); - Assert.That(activityTags, Has.Count.EqualTo(conn.Settings.Port == 5432 ? 8 : 9)); + #endregion Binary export - Assert.That(activityTags["db.query.text"], Is.EqualTo("SELECT * FROM non_existing_table")); - Assert.That(activityTags["db.system.name"], Is.EqualTo("postgresql")); - Assert.That(activityTags["db.namespace"], Is.EqualTo(conn.Settings.Database)); + #region Raw binary - Assert.That(activityTags["db.response.status_code"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); - Assert.That(activityTags["error.type"], Is.EqualTo(PostgresErrorCodes.UndefinedTable)); + [Test] + public async Task RawBinaryExport() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); - Assert.That(activityTags["db.npgsql.data_source"], Is.EqualTo(conn.ConnectionString)); + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + await connection.ExecuteNonQueryAsync($"INSERT INTO {table} (field_text, field_int2) VALUES ('Hello', 8)"); + + using var activityListener = StartListener(out var activities); + + // Raw binary export + var copyToCommand = $"COPY {table} (field_text, field_int2) TO STDIN BINARY"; + var buffer = new byte[1024]; + if (async) + { + await using var stream = await connection.BeginRawBinaryCopyAsync(copyToCommand); + while (await stream.ReadAsync(buffer, 0, buffer.Length) > 0) { } + } + else + { + using var stream = connection.BeginRawBinaryCopy(copyToCommand); + while (stream.Read(buffer, 0, buffer.Length) > 0) { } + } + + var activity = GetSingleActivity(activities, "COPY"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 7 : 8)); + + Assert.That(tags["db.query.text"], Is.EqualTo(copyToCommand)); + Assert.That(tags["db.operation.name"], Is.EqualTo("COPY TO")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); + + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); if (IsMultiplexing) - Assert.That(activityTags, Does.ContainKey("db.npgsql.connection_id")); + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); else - Assert.That(activityTags["db.npgsql.connection_id"], Is.EqualTo(conn.ProcessID)); + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); + + Assert.That(tags, Does.Not.ContainKey("db.npgsql.rows")); } [Test] - public async Task Configure_tracing([Values] bool async, [Values] bool batch) + public async Task RawBinaryExport_cancel() { - if (IsMultiplexing && !async) - return; + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); - var activities = new List(); + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + await connection.ExecuteNonQueryAsync($"INSERT INTO {table} (field_text, field_int2) VALUES ('Hello', 8)"); - using var activityListener = new ActivityListener + using var activityListener = StartListener(out var activities); + + var copyToCommand = $"COPY {table} (field_text, field_int2) TO STDIN BINARY"; + var buffer = new byte[1024]; + if (async) { - ShouldListenTo = source => source.Name == "Npgsql", - Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => activities.Add(activity) - }; - ActivitySource.AddActivityListener(activityListener); + await using var stream = await connection.BeginRawBinaryCopyAsync(copyToCommand); + var _ = await stream.ReadAsync(buffer, 0, buffer.Length); + await stream.CancelAsync(); + } + else + { + using var stream = connection.BeginRawBinaryCopy(copyToCommand); + var _ = stream.Read(buffer, 0, buffer.Length); + stream.Cancel(); + } - var dataSourceBuilder = CreateDataSourceBuilder(); - dataSourceBuilder.ConfigureTracing(options => + _ = GetSingleActivity(activities, "COPY"); + } + + [Test] + public async Task RawBinaryImport_cancel() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + + using var activityListener = StartListener(out var activities); + + var copyToCommand = $"COPY {table} (field_text, field_int2) FROM STDIN BINARY"; + byte[] garbage = [1, 2, 3, 4]; + if (async) { - options - .EnablePhysicalOpenTracing(enable: false) - .EnableFirstResponseEvent(enable: false) - .ConfigureCommandFilter(cmd => cmd.CommandText.Contains('2')) - .ConfigureBatchFilter(batch => batch.BatchCommands[0].CommandText.Contains('2')) - .ConfigureCommandSpanNameProvider(_ => "unknown_query") - .ConfigureBatchSpanNameProvider(_ => "unknown_query") - .ConfigureCommandEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")) - .ConfigureBatchEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")); + await using var stream = await connection.BeginRawBinaryCopyAsync(copyToCommand); + await stream.WriteAsync(garbage); + await stream.FlushAsync(); + await stream.CancelAsync(); + } + else + { + using var stream = connection.BeginRawBinaryCopy(copyToCommand); + stream.Write(garbage); + stream.Flush(); + stream.Cancel(); + } + + _ = GetSingleActivity(activities, "COPY"); + } + + [Test] + public async Task RawBinaryImport_error() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + var copyFromCommand = $"COPY non_existing_table (field_text, field_int2) FROM STDIN BINARY"; + var ex = Assert.ThrowsAsync(async () => + { + await using var stream = async + ? await connection.BeginRawBinaryCopyAsync(copyFromCommand) + : connection.BeginRawBinaryCopy(copyFromCommand); }); - await using var dataSource = dataSourceBuilder.Build(); + + var activity = GetSingleActivity(activities, "COPY", "COPY", ActivityStatusCode.Error, PostgresErrorCodes.UndefinedTable); + + Assert.That(activity.Events.Count(), Is.EqualTo(1)); + var exceptionEvent = activity.Events.First(); + Assert.That(exceptionEvent.Name, Is.EqualTo("exception")); + + var exceptionTags = exceptionEvent.Tags.ToDictionary(t => t.Key, t => t.Value); + Assert.That(exceptionTags, Has.Count.EqualTo(3)); + + Assert.That(exceptionTags["exception.type"], Is.EqualTo("Npgsql.PostgresException")); + Assert.That(exceptionTags["exception.message"], Does.Contain("relation \"non_existing_table\" does not exist")); + Assert.That(exceptionTags["exception.stacktrace"], Does.Contain("relation \"non_existing_table\" does not exist")); + } + + #endregion Raw binary + + #region Text COPY + + [Test] + public async Task TextImport() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + + using var activityListener = StartListener(out var activities); + + var copyFromCommand = $"COPY {table} (field_text, field_int2) FROM STDIN"; + + if (async) + { + await using var writer = await connection.BeginTextImportAsync(copyFromCommand); + await writer.WriteAsync("Hello\t8\n"); + } + else + { + using var writer = connection.BeginTextImport(copyFromCommand); + writer.Write("Hello\t8\n"); + } + + var activity = GetSingleActivity(activities, "COPY FROM"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 7 : 8)); + + Assert.That(tags["db.query.text"], Is.EqualTo(copyFromCommand)); + Assert.That(tags["db.operation.name"], Is.EqualTo("COPY FROM")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); + + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); + + if (IsMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); + else + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); + + Assert.That(tags, Does.Not.ContainKey("db.npgsql.rows")); + } + + [Test] + public async Task TextExport() + { + await using var dataSource = CreateDataSource(ds => ds.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + var table = await CreateTempTable(connection, "field_text TEXT, field_int2 SMALLINT"); + + var insertCmd = $"INSERT INTO {table} (field_text, field_int2) VALUES ('Hello', 8)"; + await connection.ExecuteNonQueryAsync(insertCmd); + + using var activityListener = StartListener(out var activities); + + var copyFromCommand = $"COPY {table} (field_text, field_int2) TO STDIN"; + + var chars = new char[30]; + if (async) + { + await using var reader = await connection.BeginTextExportAsync(copyFromCommand); + _ = await reader.ReadAsync(chars); + } + else + { + using var reader = connection.BeginTextExport(copyFromCommand); + _ = reader.Read(chars); + } + + var activity = GetSingleActivity(activities, "COPY TO"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + + Assert.That(tags, Has.Count.EqualTo(connection.Settings.Port == 5432 ? 7 : 8)); + + Assert.That(tags["db.query.text"], Is.EqualTo(copyFromCommand)); + Assert.That(tags["db.operation.name"], Is.EqualTo("COPY TO")); + Assert.That(tags["db.system.name"], Is.EqualTo("postgresql")); + Assert.That(tags["db.namespace"], Is.EqualTo(connection.Settings.Database)); + + Assert.That(tags["db.npgsql.data_source"], Is.EqualTo(connection.ConnectionString)); + + if (IsMultiplexing) + Assert.That(tags, Does.ContainKey("db.npgsql.connection_id")); + else + Assert.That(tags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); + + Assert.That(tags, Does.Not.ContainKey("db.npgsql.rows")); + } + + // Text COPY is implemented over NpgsqlRawCopyStream internally, without any additional tracing-related logic. + // So we do only basic direct coverage and depend on the general raw tests for the rest. + + #endregion Text COPY + + // All ConfigureTracing() aspects of COPY are implemented in a single code path for all COPY paths, so we test just one. + + [Test] + public async Task Copy_ConfigureTracing() + { + await using var dataSource = CreateDataSource(builder => builder.ConfigureTracing(options => + options + .EnablePhysicalOpenTracing(false) + .ConfigureCopyOperationFilter(command => command.Contains("filter_in")) + .ConfigureCopyOperationSpanNameProvider(_ => "custom_binary_import") + .ConfigureCopyOperationEnrichmentCallback((activity, _) => activity.AddTag("custom_tag", "custom_value")))); + await using var conn = await dataSource.OpenConnectionAsync(); - // We disabled physical open tracing - Assert.That(activities.Count, Is.EqualTo(0)); + var table = await CreateTempTable(conn, "field_text TEXT, field_int_filter_in SMALLINT"); + var copyCommand = $"COPY {table} (field_text, field_int_filter_in) FROM STDIN BINARY"; - await ExecuteScalar(conn, async, batch, "SELECT 1"); + var filteredOutTable = await CreateTempTable(conn, "field_text TEXT, field_int_filter_out SMALLINT"); + var filteredOutCopyCommand = $"COPY {filteredOutTable} (field_text, field_int_filter_out) FROM STDIN BINARY"; - Assert.That(activities, Is.Empty); + using var activityListener = StartListener(out var activities); - await ExecuteScalar(conn, async, batch, "SELECT 2"); - Assert.That(activities, Has.Count.EqualTo(1)); - var activity = activities[0]; - Assert.That(activity.DisplayName, Is.EqualTo("unknown_query")); - Assert.That(activity.OperationName, Is.EqualTo("unknown_query")); + if (async) + { + await using (var writer = await conn.BeginBinaryImportAsync(copyCommand)) + { + await writer.CompleteAsync(); + } + + await using (var writer = await conn.BeginBinaryImportAsync(filteredOutCopyCommand)) + { + await writer.CompleteAsync(); + } + } + else + { + using (var writer = conn.BeginBinaryImport(copyCommand)) + { + writer.Complete(); + } + + using (var writer = conn.BeginBinaryImport(filteredOutCopyCommand)) + { + writer.Complete(); + } + } - Assert.That(activity.Events.Count(), Is.EqualTo(0)); + // There should be just one activity since one of the two COPY commands is filtered out + var activity = GetSingleActivity(activities, "custom_binary_import", "custom_binary_import"); var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); Assert.That(tags["custom_tag"], Is.EqualTo("custom_value")); } - async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query) + static ActivityListener StartListener(out List activities) + { + var a = new List(); + + var activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Npgsql", + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => a.Add(activity) + }; + ActivitySource.AddActivityListener(activityListener); + + activities = a; + return activityListener; + } + + static Activity GetSingleActivity( + List activities, + string? expectedDisplayName, + string? expectedOperationName = null, + ActivityStatusCode? expectedStatusCode = null, + string? expectedStatusDescription = null) + { + Assert.That(activities, Has.Count.EqualTo(1)); + var activity = activities[0]; + Assert.That(activity.DisplayName, Is.EqualTo(expectedDisplayName)); + Assert.That(activity.OperationName, Is.EqualTo(expectedOperationName ?? expectedDisplayName)); + Assert.That(activity.Status, Is.EqualTo(expectedStatusCode ?? ActivityStatusCode.Unset)); + Assert.That(activity.StatusDescription, Is.EqualTo(expectedStatusDescription)); + + return activity; + } + + static async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query) { if (!isBatch) { From 4cf1d33347b7de18835942b0519ce2b6e8c2249d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 22 Nov 2025 15:26:55 +0100 Subject: [PATCH 152/155] Add db.npgsql.prepared to tracing (#6346) Closes #4636 --- src/Npgsql/MultiplexingDataSource.cs | 11 +++- src/Npgsql/NpgsqlActivitySource.cs | 5 +- src/Npgsql/NpgsqlCommand.cs | 13 +++-- test/Npgsql.Tests/TracingTests.cs | 87 +++++++++++++++++++++++++--- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/src/Npgsql/MultiplexingDataSource.cs b/src/Npgsql/MultiplexingDataSource.cs index 60ba882923..8c529cf7e7 100644 --- a/src/Npgsql/MultiplexingDataSource.cs +++ b/src/Npgsql/MultiplexingDataSource.cs @@ -236,11 +236,20 @@ bool WriteCommand(NpgsqlConnector connector, NpgsqlCommand command, ref Multiple // to buffer in memory), and the actual flush will occur at the level above. For cases where the // command overflows the buffer, async I/O is done, and we schedule continuations separately - // but the main thread continues to handle other commands on other connectors. + + var fullyPrepared = _autoPrepare; + if (_autoPrepare) { // TODO: Need to log based on numPrepared like in non-multiplexing mode... for (var i = 0; i < command.InternalBatchCommands.Count; i++) - command.InternalBatchCommands[i].TryAutoPrepare(connector); + if (!command.InternalBatchCommands[i].TryAutoPrepare(connector)) + fullyPrepared = false; + } + + if (command.CurrentActivity is not null && fullyPrepared) + { + command.CurrentActivity.SetTag("db.npgsql.prepared", true); } var written = connector.CommandsInFlightWriter!.TryWrite(command); diff --git a/src/Npgsql/NpgsqlActivitySource.cs b/src/Npgsql/NpgsqlActivitySource.cs index 91da0f0548..be4e257c48 100644 --- a/src/Npgsql/NpgsqlActivitySource.cs +++ b/src/Npgsql/NpgsqlActivitySource.cs @@ -16,7 +16,7 @@ static class NpgsqlActivitySource internal static bool IsEnabled => Source.HasListeners(); - internal static Activity? CommandStart(NpgsqlConnectionStringBuilder settings, string commandText, CommandType commandType, string? spanName) + internal static Activity? CommandStart(string commandText, CommandType commandType, bool? prepared, string? spanName) { string? operationName = null; @@ -49,6 +49,9 @@ static class NpgsqlActivitySource activity.SetTag("db.query.text", commandText); + if (prepared is true) + activity.SetTag("db.npgsql.prepared", true); + switch (commandType) { case CommandType.StoredProcedure: diff --git a/src/Npgsql/NpgsqlCommand.cs b/src/Npgsql/NpgsqlCommand.cs index cfd48189ec..ffbf86029f 100644 --- a/src/Npgsql/NpgsqlCommand.cs +++ b/src/Npgsql/NpgsqlCommand.cs @@ -1429,6 +1429,8 @@ internal virtual async ValueTask ExecuteReader(bool async, Com try { + var fullyPrepared = false; + switch (IsExplicitlyPrepared) { case true: @@ -1463,6 +1465,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com NpgsqlEventSource.Log.CommandStartPrepared(); connector.DataSource.MetricsReporter.CommandStartPrepared(); + fullyPrepared = true; break; case false: @@ -1502,6 +1505,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com { NpgsqlEventSource.Log.CommandStartPrepared(); connector.DataSource.MetricsReporter.CommandStartPrepared(); + fullyPrepared = true; } } @@ -1524,7 +1528,7 @@ internal virtual async ValueTask ExecuteReader(bool async, Com NpgsqlEventSource.Log.CommandStart(CommandText); startTimestamp = connector.DataSource.MetricsReporter.ReportCommandStart(); - TraceCommandStart(connector.Settings, connector.DataSource.Configuration.TracingOptions); + TraceCommandStart(connector.DataSource.Configuration.TracingOptions, fullyPrepared); TraceCommandEnrich(connector); // We do not wait for the entire send to complete before proceeding to reading - @@ -1594,7 +1598,8 @@ internal virtual async ValueTask ExecuteReader(bool async, Com State = CommandState.InProgress; - TraceCommandStart(conn.Settings, conn.NpgsqlDataSource.Configuration.TracingOptions); + // In multiplexing, we don't yet know whether the command will execute as prepared or not; that will be determined later. + TraceCommandStart(conn.NpgsqlDataSource.Configuration.TracingOptions, prepared: null); // TODO: Experiment: do we want to wait on *writing* here, or on *reading*? // Previous behavior was to wait on reading, which throw the exception from ExecuteReader (and not from @@ -1739,7 +1744,7 @@ internal void Reset() #region Tracing - internal void TraceCommandStart(NpgsqlConnectionStringBuilder settings, NpgsqlTracingOptions tracingOptions) + internal void TraceCommandStart(NpgsqlTracingOptions tracingOptions, bool? prepared) { Debug.Assert(CurrentActivity is null); @@ -1756,9 +1761,9 @@ internal void TraceCommandStart(NpgsqlConnectionStringBuilder settings, NpgsqlTr : tracingOptions.CommandSpanNameProvider?.Invoke(this); CurrentActivity = NpgsqlActivitySource.CommandStart( - settings, WrappingBatch is not null ? GetBatchFullCommandText() : CommandText, CommandType, + prepared, spanName); } } diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 3ed31d6a30..9fe94b7216 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -197,6 +197,56 @@ public async Task CommandExecute_error([Values] bool batch) Assert.That(activityTags["db.npgsql.connection_id"], Is.EqualTo(connection.ProcessID)); } + [Test] + public async Task CommandExecute_explicit_prepare([Values] bool batch) + { + if (IsMultiplexing) + { + Assert.Ignore("Explicit prepare is not supported with multiplexing"); + return; + } + + await using var dataSource = CreateDataSource(o => o.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false))); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + await ExecuteScalar(connection, async, batch, "SELECT 42", prepare: false); + var activity = GetSingleActivity(activities, "postgresql", "postgresql"); + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Does.Not.ContainKey("db.npgsql.prepared")); + + activities.Clear(); + await ExecuteScalar(connection, async, batch, "SELECT 42", prepare: true); + activity = GetSingleActivity(activities, "postgresql", "postgresql"); + tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags["db.npgsql.prepared"], Is.True); + } + + [Test] + public async Task CommandExecute_auto_prepare([Values] bool batch) + { + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.MaxAutoPrepare = 10; + dataSourceBuilder.ConnectionStringBuilder.AutoPrepareMinUsages = 2; + dataSourceBuilder.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false)); + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + await ExecuteScalar(connection, async, batch, "SELECT 42"); + var activity = GetSingleActivity(activities, "postgresql", "postgresql"); + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags, Does.Not.ContainKey("db.npgsql.prepared")); + + activities.Clear(); + await ExecuteScalar(connection, async, batch, "SELECT 42"); + activity = GetSingleActivity(activities, "postgresql", "postgresql"); + tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + Assert.That(tags["db.npgsql.prepared"], Is.True); + } + [Test] public async Task CommandExecute_ConfigureTracing([Values] bool batch) { @@ -793,26 +843,45 @@ static Activity GetSingleActivity( return activity; } - static async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query) + static async Task ExecuteScalar(NpgsqlConnection connection, bool async, bool isBatch, string query, bool prepare = false) { - if (!isBatch) - { - if (async) - return await connection.ExecuteScalarAsync(query); - else - return connection.ExecuteScalar(query); - } - else + if (isBatch) { await using var batch = connection.CreateBatch(); var batchCommand = batch.CreateBatchCommand(); batchCommand.CommandText = query; batch.BatchCommands.Add(batchCommand); + if (prepare) + { + if (async) + await batch.PrepareAsync(); + else + batch.Prepare(); + } + if (async) return await batch.ExecuteScalarAsync(); else return batch.ExecuteScalar(); } + else + { + await using var command = connection.CreateCommand(); + command.CommandText = query; + + if (prepare) + { + if (async) + await command.PrepareAsync(); + else + command.Prepare(); + } + + if (async) + return await command.ExecuteScalarAsync(); + else + return command.ExecuteScalar(); + } } } From a77ee4a48af52bf9dea9b9b8dd6aff70abc58157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Andr=C3=A9?= <2341261+manandre@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:47:07 +0100 Subject: [PATCH 153/155] Update devcontainer for .NET 10 (#6347) * Update devcontainer for .NET 10 * Use devcontainer features for dotnet * Remove useless setting --- .devcontainer/devcontainer.json | 17 ++++++++++------- .devcontainer/docker-compose.yml | 2 +- .devcontainer/dotnet/Dockerfile | 5 ----- .vscode/extensions.json | 6 +----- .vscode/settings.json | 3 +-- 5 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 .devcontainer/dotnet/Dockerfile diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1cdf7d0550..1efa473614 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,6 +5,13 @@ "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "10.0", + "dotnetRuntimeVersions": "8.0,9.0" + } + }, + "customizations": { "vscode": { "settings": { @@ -21,18 +28,14 @@ "extensions": [ "ms-dotnettools.csharp", - "formulahendry.dotnet-test-explorer", + "ms-dotnettools.csdevkit", "ms-azuretools.vscode-docker", "mutantdino.resourcemonitor" ] } }, - - "forwardPorts": [5432, 5050], - "remoteEnv": { - "DeveloperBuild": "True" - }, + "forwardPorts": [5432, 5050], - "postCreateCommand": "dotnet restore Npgsql.sln" + "postCreateCommand": "dotnet restore Npgsql.slnx" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 99d9177380..3926f919de 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: npgsql-dev: - build: ./dotnet + image: mcr.microsoft.com/devcontainers/base:ubuntu volumes: - ..:/workspace:cached tty: true diff --git a/.devcontainer/dotnet/Dockerfile b/.devcontainer/dotnet/Dockerfile deleted file mode 100644 index 66aaa421c9..0000000000 --- a/.devcontainer/dotnet/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# Source for tags: https://mcr.microsoft.com/v2/dotnet/sdk/tags/list -FROM mcr.microsoft.com/dotnet/sdk:9.0 - -# "install" the .NET 8 runtime -COPY --from=mcr.microsoft.com/dotnet/sdk:8.0 /usr/share/dotnet/shared /usr/share/dotnet/shared \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6bf5ff40c5..a505eb8cfc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,10 +2,6 @@ // List of extensions which should be recommended for users of this workspace. "recommendations": [ "ms-dotnettools.csharp", - "formulahendry.dotnet-test-explorer", - ], - // List of extensions recommended by VS Code that should not be recommended for users of this workspace. - "unwantedRecommendations": [ - + "ms-dotnettools.csdevkit" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 4e57c5f1c2..22993a3100 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,3 @@ { - "dotnet-test-explorer.testProjectPath": "**/*.Tests.csproj", - "dotnet.defaultSolution": "Npgsql.sln" + "dotnet.defaultSolution": "Npgsql.slnx" } \ No newline at end of file From 563ebf15e65e53cd00e0b05af726673685e4c590 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 22 Nov 2025 15:43:16 +0100 Subject: [PATCH 154/155] Bump version to 10.0.0 (GA) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a7f8f89bff..de99b4fd8c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 10.0.0-rtm + 10.0.0 latest true enable From a18021849f244716d3b68eefd705677f131f9ace Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 22 Nov 2025 17:35:35 +0100 Subject: [PATCH 155/155] Fix tracing autoprepare test --- test/Npgsql.Tests/TracingTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 9fe94b7216..1033dc7a55 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -227,6 +227,7 @@ public async Task CommandExecute_explicit_prepare([Values] bool batch) public async Task CommandExecute_auto_prepare([Values] bool batch) { var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.MaxPoolSize = 1; dataSourceBuilder.ConnectionStringBuilder.MaxAutoPrepare = 10; dataSourceBuilder.ConnectionStringBuilder.AutoPrepareMinUsages = 2; dataSourceBuilder.ConfigureTracing(o => o.EnablePhysicalOpenTracing(false));