From 5bb0fae6ce29dcb76dd91c6ddc8e8f7345b586d6 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 26 Aug 2021 21:08:51 +0200 Subject: [PATCH 01/10] Redo timestamp handling Closes #3947 Closes #3800 --- .github/workflows/build.yml | 1 + Npgsql.sln | 11 + .../Internal/GeoJSONTypeHandlerResolver.cs | 8 +- .../GeoJSONTypeHandlerResolverFactory.cs | 8 +- .../Internal/JsonNetTypeHandlerResolver.cs | 8 +- .../JsonNetTypeHandlerResolverFactory.cs | 8 +- .../NetTopologySuiteTypeHandlerResolver.cs | 8 +- ...TopologySuiteTypeHandlerResolverFactory.cs | 8 +- .../Internal/LegacyTimestampHandler.cs | 60 ++ .../Internal/LegacyTimestampTzHandler.cs | 98 +++ .../Internal/NodaTimeTypeHandlerResolver.cs | 26 +- .../NodaTimeTypeHandlerResolverFactory.cs | 9 +- src/Npgsql.NodaTime/Internal/NodaTimeUtils.cs | 45 ++ src/Npgsql.NodaTime/Internal/TimeTzHandler.cs | 21 +- .../Internal/TimestampHandler.cs | 152 +---- .../Internal/TimestampTzHandler.cs | 102 ++- .../NpgsqlNodaTimeExtensions.cs | 7 +- .../Properties/AssemblyInfo.cs | 8 + .../TypeHandlerSourceGenerator.cs | 2 +- .../DateTimeHandlers/DateTimeUtils.cs | 121 ++++ .../DateTimeHandlers/TimeTzHandler.cs | 41 +- .../DateTimeHandlers/TimestampHandler.cs | 143 +---- .../DateTimeHandlers/TimestampTzHandler.cs | 180 ++++-- ...dlerResolver.cs => TypeHandlerResolver.cs} | 10 +- ...ctory.cs => TypeHandlerResolverFactory.cs} | 15 +- src/Npgsql/NpgsqlParameter.cs | 15 +- src/Npgsql/NpgsqlParameter`.cs | 8 +- src/Npgsql/Properties/AssemblyInfo.cs | 7 + src/Npgsql/PublicAPI.Unshipped.txt | 2 +- .../PgOutput/PgOutputAsyncEnumerable.cs | 6 +- .../Replication/ReplicationConnection.cs | 6 +- .../TypeMapping/BuiltInTypeHandlerResolver.cs | 239 +++++-- .../BuiltInTypeHandlerResolverFactory.cs | 13 +- src/Npgsql/TypeMapping/ConnectorTypeMapper.cs | 48 +- src/Npgsql/TypeMapping/GlobalTypeMapper.cs | 23 +- src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs | 2 +- src/Npgsql/TypeMapping/TypeMapperBase.cs | 2 +- src/Npgsql/Util/PGUtil.cs | 9 + .../LegacyNodaTimeTests.cs | 325 ++++++++++ test/Npgsql.NodaTime.Tests/NodaTimeTests.cs | 607 ++++++++++++++++++ .../Npgsql.NodaTime.Tests.csproj | 14 + test/Npgsql.PluginTests/NodaTimeTests.cs | 520 --------------- .../Npgsql.PluginTests.csproj | 2 - test/Npgsql.Tests/ReaderTests.cs | 17 +- test/Npgsql.Tests/TestUtil.cs | 48 +- test/Npgsql.Tests/TypeMapperTests.cs | 36 +- test/Npgsql.Tests/Types/DateTimeTests.cs | 578 +++++++++++++---- .../Npgsql.Tests/Types/LegacyDateTimeTests.cs | 256 ++++++++ 48 files changed, 2578 insertions(+), 1305 deletions(-) create mode 100644 src/Npgsql.NodaTime/Internal/LegacyTimestampHandler.cs create mode 100644 src/Npgsql.NodaTime/Internal/LegacyTimestampTzHandler.cs create mode 100644 src/Npgsql.NodaTime/Internal/NodaTimeUtils.cs create mode 100644 src/Npgsql.NodaTime/Properties/AssemblyInfo.cs create mode 100644 src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs rename src/Npgsql/Internal/TypeHandling/{ITypeHandlerResolver.cs => TypeHandlerResolver.cs} (69%) rename src/Npgsql/Internal/TypeHandling/{ITypeHandlerResolverFactory.cs => TypeHandlerResolverFactory.cs} (58%) create mode 100644 test/Npgsql.NodaTime.Tests/LegacyNodaTimeTests.cs create mode 100644 test/Npgsql.NodaTime.Tests/NodaTimeTests.cs create mode 100644 test/Npgsql.NodaTime.Tests/Npgsql.NodaTime.Tests.csproj delete mode 100644 test/Npgsql.PluginTests/NodaTimeTests.cs create mode 100644 test/Npgsql.Tests/Types/LegacyDateTimeTests.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb3595a194..287ed1b038 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -263,6 +263,7 @@ jobs: run: | if [ -z "${{ matrix.pg_prerelease }}" ]; then dotnet test -c ${{ matrix.config }} test/Npgsql.PluginTests --logger "GitHubActions;report-warnings=false" + dotnet test -c ${{ matrix.config }} test/Npgsql.NodaTime.Tests --logger "GitHubActions;report-warnings=false" fi shell: bash diff --git a/Npgsql.sln b/Npgsql.sln index de284b2c23..3ffc51f6cb 100644 --- a/Npgsql.sln +++ b/Npgsql.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.SourceGenerators", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.TrimmingTests", "test\Npgsql.TrimmingTests\Npgsql.TrimmingTests.csproj", "{844EC023-21B8-448D-93AD-5F6857F15DFF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Npgsql.NodaTime.Tests", "test\Npgsql.NodaTime.Tests\Npgsql.NodaTime.Tests.csproj", "{C00D2EB1-5719-4372-9E1C-5ED05DC23A00}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -133,6 +135,14 @@ Global {844EC023-21B8-448D-93AD-5F6857F15DFF}.Release|Any CPU.Build.0 = Release|Any CPU {844EC023-21B8-448D-93AD-5F6857F15DFF}.Release|x86.ActiveCfg = Release|Any CPU {844EC023-21B8-448D-93AD-5F6857F15DFF}.Release|x86.Build.0 = Release|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|x86.ActiveCfg = Debug|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Debug|x86.Build.0 = Debug|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|Any CPU.Build.0 = Release|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|x86.ActiveCfg = Release|Any CPU + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -149,6 +159,7 @@ Global {A77E5FAF-D775-4AB4-8846-8965C2104E60} = {ED612DB1-AB32-4603-95E7-891BACA71C39} {63026A19-60B8-4906-81CB-216F30E8094B} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {844EC023-21B8-448D-93AD-5F6857F15DFF} = {ED612DB1-AB32-4603-95E7-891BACA71C39} + {C00D2EB1-5719-4372-9E1C-5ED05DC23A00} = {ED612DB1-AB32-4603-95E7-891BACA71C39} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C90AEECD-DB4C-4BE6-B506-16A449852FB8} diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs index 34f3a8cff3..ba8e320fd3 100644 --- a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs +++ b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs @@ -13,7 +13,7 @@ namespace Npgsql.GeoJSON.Internal { - public class GeoJSONTypeHandlerResolver : ITypeHandlerResolver + public class GeoJSONTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlDatabaseInfo _databaseInfo; readonly GeoJsonHandler _geometryHandler, _geographyHandler; @@ -51,7 +51,7 @@ internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions op _geographyHandler = new GeoJsonHandler(pgGeographyType, options, crsMap); } - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName switch { "geometry" => _geometryHandler, @@ -59,7 +59,7 @@ internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions op _ => null }; - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => ClrTypeToDataTypeName(type, _geographyAsDefault) is { } dataTypeName && ResolveByDataTypeName(dataTypeName) is { } handler ? handler : null; @@ -71,7 +71,7 @@ internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions op ? "geography" : "geometry"; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => DoGetMappingByDataTypeName(dataTypeName); internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName) diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs index d9fe364317..e3731247fa 100644 --- a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs +++ b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolverFactory.cs @@ -5,7 +5,7 @@ namespace Npgsql.GeoJSON.Internal { - public class GeoJSONTypeHandlerResolverFactory : ITypeHandlerResolverFactory + public class GeoJSONTypeHandlerResolverFactory : TypeHandlerResolverFactory { readonly GeoJSONOptions _options; readonly bool _geographyAsDefault; @@ -13,13 +13,13 @@ public class GeoJSONTypeHandlerResolverFactory : ITypeHandlerResolverFactory public GeoJSONTypeHandlerResolverFactory(GeoJSONOptions options, bool geographyAsDefault) => (_options, _geographyAsDefault) = (options, geographyAsDefault); - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new GeoJSONTypeHandlerResolver(connector, _options, _geographyAsDefault); - public string? GetDataTypeNameByClrType(Type type) + public override string? GetDataTypeNameByClrType(Type type) => GeoJSONTypeHandlerResolver.ClrTypeToDataTypeName(type, _geographyAsDefault); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => GeoJSONTypeHandlerResolver.DoGetMappingByDataTypeName(dataTypeName); } } diff --git a/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolver.cs b/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolver.cs index 6f776070d1..6a42820e85 100644 --- a/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolver.cs +++ b/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolver.cs @@ -10,7 +10,7 @@ namespace Npgsql.Json.NET.Internal { - public class JsonNetTypeHandlerResolver : ITypeHandlerResolver + public class JsonNetTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlDatabaseInfo _databaseInfo; readonly JsonbHandler _jsonbHandler; @@ -38,7 +38,7 @@ internal JsonNetTypeHandlerResolver( _ => null }; - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName switch { "jsonb" => _jsonbHandler, @@ -46,7 +46,7 @@ internal JsonNetTypeHandlerResolver( _ => null }; - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => ClrTypeToDataTypeName(type, _dataTypeNamesByClrType) is { } dataTypeName && ResolveByDataTypeName(dataTypeName) is { } handler ? handler : null; @@ -54,7 +54,7 @@ internal JsonNetTypeHandlerResolver( internal static string? ClrTypeToDataTypeName(Type type, Dictionary clrTypes) => clrTypes.TryGetValue(type, out var dataTypeName) ? dataTypeName : null; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => DoGetMappingByDataTypeName(dataTypeName); internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName) diff --git a/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolverFactory.cs b/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolverFactory.cs index 112ad33d5c..2c2debb9bb 100644 --- a/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolverFactory.cs +++ b/src/Npgsql.Json.NET/Internal/JsonNetTypeHandlerResolverFactory.cs @@ -7,7 +7,7 @@ namespace Npgsql.Json.NET.Internal { - public class JsonNetTypeHandlerResolverFactory : ITypeHandlerResolverFactory + public class JsonNetTypeHandlerResolverFactory : TypeHandlerResolverFactory { readonly Type[] _jsonbClrTypes; readonly Type[] _jsonClrTypes; @@ -34,13 +34,13 @@ public JsonNetTypeHandlerResolverFactory( _byType[type] = "json"; } - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new JsonNetTypeHandlerResolver(connector, _byType, _settings); - public string? GetDataTypeNameByClrType(Type type) + public override string? GetDataTypeNameByClrType(Type type) => JsonNetTypeHandlerResolver.ClrTypeToDataTypeName(type, _byType); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => JsonNetTypeHandlerResolver.DoGetMappingByDataTypeName(dataTypeName); } diff --git a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs index b045e7fcc8..953d08e02d 100644 --- a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs +++ b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs @@ -10,7 +10,7 @@ namespace Npgsql.NetTopologySuite.Internal { - public class NetTopologySuiteTypeHandlerResolver : ITypeHandlerResolver + public class NetTopologySuiteTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlDatabaseInfo _databaseInfo; readonly bool _geographyAsDefault; @@ -37,7 +37,7 @@ internal NetTopologySuiteTypeHandlerResolver( _geographyHandler = new NetTopologySuiteHandler(pgGeographyType, reader, writer); } - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName switch { "geometry" => _geometryHandler, @@ -45,7 +45,7 @@ internal NetTopologySuiteTypeHandlerResolver( _ => null }; - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => ClrTypeToDataTypeName(type, _geographyAsDefault) is { } dataTypeName && ResolveByDataTypeName(dataTypeName) is { } handler ? handler : null; @@ -57,7 +57,7 @@ internal NetTopologySuiteTypeHandlerResolver( ? "geography" : "geometry"; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => DoGetMappingByDataTypeName(dataTypeName); internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName) diff --git a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolverFactory.cs b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolverFactory.cs index f69ac1d5ef..2581a9f283 100644 --- a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolverFactory.cs +++ b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolverFactory.cs @@ -8,7 +8,7 @@ namespace Npgsql.NetTopologySuite.Internal { - public class NetTopologySuiteTypeHandlerResolverFactory : ITypeHandlerResolverFactory + public class NetTopologySuiteTypeHandlerResolverFactory : TypeHandlerResolverFactory { readonly CoordinateSequenceFactory _coordinateSequenceFactory; readonly PrecisionModel _precisionModel; @@ -27,14 +27,14 @@ public NetTopologySuiteTypeHandlerResolverFactory( _geographyAsDefault = geographyAsDefault; } - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new NetTopologySuiteTypeHandlerResolver(connector, _coordinateSequenceFactory, _precisionModel, _handleOrdinates, _geographyAsDefault); - public string? GetDataTypeNameByClrType(Type type) + public override string? GetDataTypeNameByClrType(Type type) => NetTopologySuiteTypeHandlerResolver.ClrTypeToDataTypeName(type, _geographyAsDefault); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => NetTopologySuiteTypeHandlerResolver.DoGetMappingByDataTypeName(dataTypeName); } } diff --git a/src/Npgsql.NodaTime/Internal/LegacyTimestampHandler.cs b/src/Npgsql.NodaTime/Internal/LegacyTimestampHandler.cs new file mode 100644 index 0000000000..07f0ca14ff --- /dev/null +++ b/src/Npgsql.NodaTime/Internal/LegacyTimestampHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Diagnostics; +using NodaTime; +using Npgsql.BackendMessages; +using Npgsql.Internal; +using Npgsql.Internal.TypeHandling; +using Npgsql.PostgresTypes; +using BclTimestampHandler = Npgsql.Internal.TypeHandlers.DateTimeHandlers.TimestampHandler; + +namespace Npgsql.NodaTime.Internal +{ + sealed partial class LegacyTimestampHandler : NpgsqlSimpleTypeHandler, + INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler + { + readonly bool _convertInfinityDateTime; + readonly BclTimestampHandler _bclHandler; + + internal LegacyTimestampHandler(PostgresType postgresType, bool convertInfinityDateTime) + : base(postgresType) + { + _convertInfinityDateTime = convertInfinityDateTime; + _bclHandler = new BclTimestampHandler(postgresType, convertInfinityDateTime); + } + + #region Read + + public override Instant Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) + => TimestampTzHandler.ReadInstant(buf, _convertInfinityDateTime); + + LocalDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => TimestampHandler.ReadLocalDateTime(buf); + + DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _bclHandler.Read(buf, len, fieldDescription); + + #endregion Read + + #region Write + + public override int ValidateAndGetLength(Instant value, NpgsqlParameter? parameter) + => 8; + + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(LocalDateTime value, NpgsqlParameter? parameter) + => 8; + + public override void Write(Instant value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => TimestampTzHandler.WriteInstant(value, buf, _convertInfinityDateTime); + + void INpgsqlSimpleTypeHandler.Write(LocalDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => TimestampHandler.WriteLocalDateTime(value, buf); + + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_bclHandler).ValidateAndGetLength(value, parameter); + + void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_bclHandler).Write(value, buf, parameter); + + #endregion Write + } +} diff --git a/src/Npgsql.NodaTime/Internal/LegacyTimestampTzHandler.cs b/src/Npgsql.NodaTime/Internal/LegacyTimestampTzHandler.cs new file mode 100644 index 0000000000..85459079df --- /dev/null +++ b/src/Npgsql.NodaTime/Internal/LegacyTimestampTzHandler.cs @@ -0,0 +1,98 @@ +using System; +using NodaTime; +using NodaTime.TimeZones; +using Npgsql.BackendMessages; +using Npgsql.Internal; +using Npgsql.Internal.TypeHandling; +using Npgsql.PostgresTypes; +using BclTimestampTzHandler = Npgsql.Internal.TypeHandlers.DateTimeHandlers.TimestampTzHandler; + +namespace Npgsql.NodaTime.Internal +{ + sealed partial class LegacyTimestampTzHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, + INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, + INpgsqlSimpleTypeHandler + { + readonly IDateTimeZoneProvider _dateTimeZoneProvider; + readonly TimestampTzHandler _wrappedHandler; + + public LegacyTimestampTzHandler(PostgresType postgresType, bool convertInfinityDateTime) + : base(postgresType) + { + _dateTimeZoneProvider = DateTimeZoneProviders.Tzdb; + _wrappedHandler = new TimestampTzHandler(postgresType, convertInfinityDateTime); + } + + #region Read + + public override Instant Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) + => _wrappedHandler.Read(buf, len, fieldDescription); + + ZonedDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + { + try + { + var zonedDateTime = ((INpgsqlSimpleTypeHandler)_wrappedHandler).Read(buf, len, fieldDescription); + + var value = buf.ReadInt64(); + if (value == long.MaxValue || value == long.MinValue) + throw new NotSupportedException("Infinity values not supported for timestamp with time zone"); + return zonedDateTime.WithZone(_dateTimeZoneProvider[buf.Connection.Timezone]); + } + catch (Exception e) when ( + string.Equals(buf.Connection.Timezone, "localtime", StringComparison.OrdinalIgnoreCase) && + (e is TimeZoneNotFoundException || e is DateTimeZoneNotFoundException)) + { + throw new TimeZoneNotFoundException( + "The special PostgreSQL timezone 'localtime' is not supported when reading values of type 'timestamp with time zone'. " + + "Please specify a real timezone in 'postgresql.conf' on the server, or set the 'PGTZ' environment variable on the client.", + e); + } + } + + OffsetDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => ((INpgsqlSimpleTypeHandler)this).Read(buf, len, fieldDescription).ToOffsetDateTime(); + + DateTimeOffset INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _wrappedHandler.Read(buf, len, fieldDescription); + + DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _wrappedHandler.Read(buf, len, fieldDescription); + + #endregion Read + + #region Write + + public override int ValidateAndGetLength(Instant value, NpgsqlParameter? parameter) + => 8; + + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(ZonedDateTime value, NpgsqlParameter? parameter) + => 8; + + public int ValidateAndGetLength(OffsetDateTime value, NpgsqlParameter? parameter) + => 8; + + public override void Write(Instant value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => _wrappedHandler.Write(value, buf, parameter); + + void INpgsqlSimpleTypeHandler.Write(ZonedDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => _wrappedHandler.Write(value.ToInstant(), buf, parameter); + + public void Write(OffsetDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => _wrappedHandler.Write(value.ToInstant(), buf, parameter); + + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_wrappedHandler).ValidateAndGetLength(value, parameter); + + void INpgsqlSimpleTypeHandler.Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_wrappedHandler).Write(value, buf, parameter); + + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_wrappedHandler).ValidateAndGetLength(value, parameter); + + void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => ((INpgsqlSimpleTypeHandler)_wrappedHandler).Write(value, buf, parameter); + + #endregion Write + } +} diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs index 39e38afa22..fdfead4c28 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs @@ -6,15 +6,16 @@ using Npgsql.PostgresTypes; using Npgsql.TypeMapping; using NpgsqlTypes; +using static Npgsql.NodaTime.Internal.NodaTimeUtils; namespace Npgsql.NodaTime.Internal { - public class NodaTimeTypeHandlerResolver : ITypeHandlerResolver + public class NodaTimeTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlDatabaseInfo _databaseInfo; - readonly TimestampHandler _timestampHandler; - readonly TimestampTzHandler _timestampTzHandler; + readonly NpgsqlTypeHandler _timestampHandler; + readonly NpgsqlTypeHandler _timestampTzHandler; readonly DateHandler _dateHandler; readonly TimeHandler _timeHandler; readonly TimeTzHandler _timeTzHandler; @@ -24,15 +25,19 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector) { _databaseInfo = connector.DatabaseInfo; - _timestampHandler = new TimestampHandler(PgType("timestamp without time zone"), connector.Settings.ConvertInfinityDateTime); - _timestampTzHandler = new TimestampTzHandler(PgType("timestamp with time zone"), connector.Settings.ConvertInfinityDateTime); + _timestampHandler = LegacyTimestampBehavior + ? new LegacyTimestampHandler(PgType("timestamp without time zone"), connector.Settings.ConvertInfinityDateTime) + : new TimestampHandler(PgType("timestamp without time zone"), connector.Settings.ConvertInfinityDateTime); + _timestampTzHandler = LegacyTimestampBehavior + ? new LegacyTimestampTzHandler(PgType("timestamp with time zone"), connector.Settings.ConvertInfinityDateTime) + : new TimestampTzHandler(PgType("timestamp with time zone"), connector.Settings.ConvertInfinityDateTime); _dateHandler = new DateHandler(PgType("date"), connector.Settings.ConvertInfinityDateTime); _timeHandler = new TimeHandler(PgType("time without time zone")); _timeTzHandler = new TimeTzHandler(PgType("time with time zone")); _intervalHandler = new IntervalHandler(PgType("interval")); } - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName switch { "timestamp" or "timestamp without time zone" => _timestampHandler, @@ -45,14 +50,17 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector) _ => null }; - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => ClrTypeToDataTypeName(type) is { } dataTypeName && ResolveByDataTypeName(dataTypeName) is { } handler ? handler : null; internal static string? ClrTypeToDataTypeName(Type type) { - if (type == typeof(Instant) || type == typeof(LocalDateTime)) + if (type == typeof(Instant)) + return LegacyTimestampBehavior ? "timestamp without time zone" : "timestamp with time zone"; + + if (type == typeof(LocalDateTime)) return "timestamp without time zone"; if (type == typeof(ZonedDateTime) || type == typeof(OffsetDateTime)) return "timestamp with time zone"; @@ -68,7 +76,7 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector) return null; } - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => DoGetMappingByDataTypeName(dataTypeName); internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName) diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolverFactory.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolverFactory.cs index 0b6849d43b..1fc0c59d39 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolverFactory.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolverFactory.cs @@ -1,19 +1,18 @@ using System; using Npgsql.Internal; using Npgsql.Internal.TypeHandling; -using Npgsql.TypeMapping; namespace Npgsql.NodaTime.Internal { - public class NodaTimeTypeHandlerResolverFactory : ITypeHandlerResolverFactory + public class NodaTimeTypeHandlerResolverFactory : TypeHandlerResolverFactory { - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new NodaTimeTypeHandlerResolver(connector); - public string? GetDataTypeNameByClrType(Type type) + public override string? GetDataTypeNameByClrType(Type type) => NodaTimeTypeHandlerResolver.ClrTypeToDataTypeName(type); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => NodaTimeTypeHandlerResolver.DoGetMappingByDataTypeName(dataTypeName); } } diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeUtils.cs b/src/Npgsql.NodaTime/Internal/NodaTimeUtils.cs new file mode 100644 index 0000000000..be406714a7 --- /dev/null +++ b/src/Npgsql.NodaTime/Internal/NodaTimeUtils.cs @@ -0,0 +1,45 @@ +using System; +using NodaTime; + +namespace Npgsql.NodaTime.Internal +{ + static class NodaTimeUtils + { +#if DEBUG + internal static bool LegacyTimestampBehavior; +#else + internal static readonly bool LegacyTimestampBehavior; +#endif + + static NodaTimeUtils() + => LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled; + + static readonly Instant Instant2000 = Instant.FromUtc(2000, 1, 1, 0, 0, 0); + static readonly Duration Plus292Years = Duration.FromDays(292 * 365); + static readonly Duration Minus292Years = -Plus292Years; + + /// + /// Decodes a PostgreSQL timestamp/timestamptz into a NodaTime Instant. + /// + /// The number of microseconds from 2000-01-01T00:00:00. + /// + /// Unfortunately NodaTime doesn't have Duration.FromMicroseconds(), so we decompose into milliseconds and nanoseconds. + /// + internal static Instant DecodeInstant(long value) + => Instant2000 + Duration.FromMilliseconds(value / 1000) + Duration.FromNanoseconds(value % 1000 * 1000); + + /// + /// Encodes a NodaTime Instant to a PostgreSQL timestamp/timestamptz. + /// + internal static long EncodeInstant(Instant instant) + { + // We need to write the number of microseconds from 2000-01-01T00:00:00. + var since2000 = instant - Instant2000; + + // The nanoseconds may overflow, so fallback to BigInteger where necessary. + return since2000 >= Minus292Years && since2000 <= Plus292Years + ? since2000.ToInt64Nanoseconds() / 1000 + : (long)(since2000.ToBigIntegerNanoseconds() / 1000); + } + } +} diff --git a/src/Npgsql.NodaTime/Internal/TimeTzHandler.cs b/src/Npgsql.NodaTime/Internal/TimeTzHandler.cs index ae98c20805..13faef65d5 100644 --- a/src/Npgsql.NodaTime/Internal/TimeTzHandler.cs +++ b/src/Npgsql.NodaTime/Internal/TimeTzHandler.cs @@ -8,8 +8,7 @@ namespace Npgsql.NodaTime.Internal { - sealed partial class TimeTzHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, - INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler + sealed partial class TimeTzHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler { readonly BclTimeTzHandler _bclHandler; @@ -39,23 +38,5 @@ int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTimeOffset void INpgsqlSimpleTypeHandler.Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => _bclHandler.Write(value, buf, parameter); - - DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => _bclHandler.Read(buf, len, fieldDescription); - - int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) - => _bclHandler.ValidateAndGetLength(value, parameter); - - void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - => _bclHandler.Write(value, buf, parameter); - - TimeSpan INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => _bclHandler.Read(buf, len, fieldDescription); - - int INpgsqlSimpleTypeHandler.ValidateAndGetLength(TimeSpan value, NpgsqlParameter? parameter) - => _bclHandler.ValidateAndGetLength(value, parameter); - - void INpgsqlSimpleTypeHandler.Write(TimeSpan value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - => _bclHandler.Write(value, buf, parameter); } } diff --git a/src/Npgsql.NodaTime/Internal/TimestampHandler.cs b/src/Npgsql.NodaTime/Internal/TimestampHandler.cs index 6c28e19a79..a4981de9f0 100644 --- a/src/Npgsql.NodaTime/Internal/TimestampHandler.cs +++ b/src/Npgsql.NodaTime/Internal/TimestampHandler.cs @@ -1,173 +1,59 @@ using System; -using System.Diagnostics; using NodaTime; using Npgsql.BackendMessages; using Npgsql.Internal; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using BclTimestampHandler = Npgsql.Internal.TypeHandlers.DateTimeHandlers.TimestampHandler; +using static Npgsql.NodaTime.Internal.NodaTimeUtils; namespace Npgsql.NodaTime.Internal { - sealed partial class TimestampHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler + sealed partial class TimestampHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler { - static readonly Instant Instant0 = Instant.FromUtc(1, 1, 1, 0, 0, 0); - static readonly Instant Instant2000 = Instant.FromUtc(2000, 1, 1, 0, 0, 0); - static readonly Duration Plus292Years = Duration.FromDays(292 * 365); - static readonly Duration Minus292Years = -Plus292Years; - - /// - /// Whether to convert positive and negative infinity values to Instant.{Max,Min}Value when - /// an Instant is requested - /// - readonly bool _convertInfinityDateTime; readonly BclTimestampHandler _bclHandler; internal TimestampHandler(PostgresType postgresType, bool convertInfinityDateTime) : base(postgresType) - { - _convertInfinityDateTime = convertInfinityDateTime; - _bclHandler = new BclTimestampHandler(postgresType, convertInfinityDateTime); - } + => _bclHandler = new BclTimestampHandler(postgresType, convertInfinityDateTime); #region Read - public override Instant Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) - { - var value = buf.ReadInt64(); - if (_convertInfinityDateTime) - { - if (value == long.MaxValue) - return Instant.MaxValue; - if (value == long.MinValue) - return Instant.MinValue; - } - - return Decode(value); - } + public override LocalDateTime Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => ReadLocalDateTime(buf); - LocalDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + internal static LocalDateTime ReadLocalDateTime(NpgsqlReadBuffer buf) { var value = buf.ReadInt64(); - if (value == long.MaxValue || value == long.MinValue) - throw new NotSupportedException("Infinity values not supported when reading LocalDateTime, read as Instant instead"); - return Decode(value).InUtc().LocalDateTime; - } - // value is the number of microseconds from 2000-01-01T00:00:00. - // Unfortunately NodaTime doesn't have Duration.FromMicroseconds(), so we decompose into milliseconds - // and nanoseconds - internal static Instant Decode(long value) - => Instant2000 + Duration.FromMilliseconds(value / 1000) + Duration.FromNanoseconds(value % 1000 * 1000); + if (value == long.MaxValue || value == long.MinValue) + throw new NotSupportedException($"Infinity values not supported when reading {nameof(LocalDateTime)}"); - // This is legacy support for PostgreSQL's old floating-point timestamp encoding - finally removed in PG 10 and not used for a long - // time. Unfortunately CrateDB seems to use this for some reason. - internal static Instant Decode(double value) - { - Debug.Assert(!double.IsPositiveInfinity(value) && !double.IsNegativeInfinity(value)); - - if (value >= 0d) - { - var date = (int)value / 86400; - date += 730119; // 730119 = days since era (0001-01-01) for 2000-01-01 - var microsecondOfDay = (long)((value % 86400d) * 1000000d); - - return Instant0 + Duration.FromDays(date) + Duration.FromNanoseconds(microsecondOfDay * 1000); - } - else - { - value = -value; - var date = (int)value / 86400; - var microsecondOfDay = (long)((value % 86400d) * 1000000d); - if (microsecondOfDay != 0) - { - ++date; - microsecondOfDay = 86400000000L - microsecondOfDay; - } - - date = 730119 - date; // 730119 = days since era (0001-01-01) for 2000-01-01 - - return Instant0 + Duration.FromDays(date) + Duration.FromNanoseconds(microsecondOfDay * 1000); - } + return DecodeInstant(value).InUtc().LocalDateTime; } + DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _bclHandler.Read(buf, len, fieldDescription); + #endregion Read #region Write - public override int ValidateAndGetLength(Instant value, NpgsqlParameter? parameter) + public override int ValidateAndGetLength(LocalDateTime value, NpgsqlParameter? parameter) => 8; - int INpgsqlSimpleTypeHandler.ValidateAndGetLength(LocalDateTime value, NpgsqlParameter? parameter) - => 8; - - public override void Write(Instant value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - { - if (_convertInfinityDateTime) - { - if (value == Instant.MaxValue) - { - buf.WriteInt64(long.MaxValue); - return; - } - - if (value == Instant.MinValue) - { - buf.WriteInt64(long.MinValue); - return; - } - } - - WriteInteger(value, buf); - } + public override void Write(LocalDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => WriteLocalDateTime(value, buf); - void INpgsqlSimpleTypeHandler.Write(LocalDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - => WriteInteger(value.InUtc().ToInstant(), buf); - - // We need to write the number of microseconds from 2000-01-01T00:00:00. - internal static void WriteInteger(Instant instant, NpgsqlWriteBuffer buf) - { - var since2000 = instant - Instant2000; - - // The nanoseconds may overflow, so fallback to BigInteger where necessary. - var microseconds = - since2000 >= Minus292Years && - since2000 <= Plus292Years - ? since2000.ToInt64Nanoseconds() / 1000 - : (long)(since2000.ToBigIntegerNanoseconds() / 1000); - - buf.WriteInt64(microseconds); - } - - // This is legacy support for PostgreSQL's old floating-point timestamp encoding - finally removed in PG 10 and not used for a long - // time. Unfortunately CrateDB seems to use this for some reason. - internal static void WriteDouble(Instant instant, NpgsqlWriteBuffer buf) - { - var localDateTime = instant.InUtc().LocalDateTime; - var totalDaysSinceEra = Period.Between(default(LocalDateTime), localDateTime, PeriodUnits.Days).Days; - var secondOfDay = localDateTime.NanosecondOfDay / 1000000000d; - - if (totalDaysSinceEra >= 730119) - { - var uSecsDate = (totalDaysSinceEra - 730119) * 86400d; - buf.WriteDouble(uSecsDate + secondOfDay); - } - else - { - var uSecsDate = (730119 - totalDaysSinceEra) * 86400d; - buf.WriteDouble(-(uSecsDate - secondOfDay)); - } - } - - #endregion Write - - DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => _bclHandler.Read(buf, len, fieldDescription); + internal static void WriteLocalDateTime(LocalDateTime value, NpgsqlWriteBuffer buf) + => buf.WriteInt64(EncodeInstant(value.InUtc().ToInstant())); int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) => ((INpgsqlSimpleTypeHandler)_bclHandler).ValidateAndGetLength(value, parameter); void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => ((INpgsqlSimpleTypeHandler)_bclHandler).Write(value, buf, parameter); + + #endregion Write } } diff --git a/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs b/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs index 6c15ed301a..1baa3fc7e2 100644 --- a/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs +++ b/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs @@ -1,31 +1,24 @@ using System; using NodaTime; -using NodaTime.TimeZones; using Npgsql.BackendMessages; using Npgsql.Internal; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using BclTimestampTzHandler = Npgsql.Internal.TypeHandlers.DateTimeHandlers.TimestampTzHandler; +using static Npgsql.NodaTime.Internal.NodaTimeUtils; namespace Npgsql.NodaTime.Internal { sealed partial class TimestampTzHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, - INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, - INpgsqlSimpleTypeHandler + INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, + INpgsqlSimpleTypeHandler { - readonly IDateTimeZoneProvider _dateTimeZoneProvider; readonly BclTimestampTzHandler _bclHandler; - - /// - /// Whether to convert positive and negative infinity values to Instant.{Max,Min}Value when - /// an Instant is requested - /// readonly bool _convertInfinityDateTime; public TimestampTzHandler(PostgresType postgresType, bool convertInfinityDateTime) : base(postgresType) { - _dateTimeZoneProvider = DateTimeZoneProviders.Tzdb; _convertInfinityDateTime = convertInfinityDateTime; _bclHandler = new BclTimestampTzHandler(postgresType, convertInfinityDateTime); } @@ -33,40 +26,27 @@ public TimestampTzHandler(PostgresType postgresType, bool convertInfinityDateTim #region Read public override Instant Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) - { - var value = buf.ReadInt64(); - if (_convertInfinityDateTime) + => ReadInstant(buf, _convertInfinityDateTime); + + internal static Instant ReadInstant(NpgsqlReadBuffer buf, bool convertInfinityDateTime) + => buf.ReadInt64() switch { - if (value == long.MaxValue) - return Instant.MaxValue; - if (value == long.MinValue) - return Instant.MinValue; - } - return TimestampHandler.Decode(value); - } + long.MaxValue when convertInfinityDateTime => Instant.MaxValue, + long.MinValue when convertInfinityDateTime => Instant.MinValue, + var value => DecodeInstant(value) + }; ZonedDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - { - try - { - var value = buf.ReadInt64(); - if (value == long.MaxValue || value == long.MinValue) - throw new NotSupportedException("Infinity values not supported for timestamp with time zone"); - return TimestampHandler.Decode(value).InZone(_dateTimeZoneProvider[buf.Connection.Timezone]); - } - catch (Exception e) when ( - string.Equals(buf.Connection.Timezone, "localtime", StringComparison.OrdinalIgnoreCase) && - (e is TimeZoneNotFoundException || e is DateTimeZoneNotFoundException)) - { - throw new TimeZoneNotFoundException( - "The special PostgreSQL timezone 'localtime' is not supported when reading values of type 'timestamp with time zone'. " + - "Please specify a real timezone in 'postgresql.conf' on the server, or set the 'PGTZ' environment variable on the client.", - e); - } - } + => Read(buf, len, fieldDescription).InUtc(); OffsetDateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => ((INpgsqlSimpleTypeHandler)this).Read(buf, len, fieldDescription).ToOffsetDateTime(); + => Read(buf, len, fieldDescription).WithOffset(Offset.Zero); + + DateTimeOffset INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _bclHandler.Read(buf, len, fieldDescription); + + DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) + => _bclHandler.Read(buf, len, fieldDescription); #endregion Read @@ -76,14 +56,35 @@ public override int ValidateAndGetLength(Instant value, NpgsqlParameter? paramet => 8; int INpgsqlSimpleTypeHandler.ValidateAndGetLength(ZonedDateTime value, NpgsqlParameter? parameter) - => 8; + { + if (!LegacyTimestampBehavior && value.Zone != DateTimeZone.Utc) + { + throw new InvalidCastException( + $"Cannot write ZonedDateTime with Zone={value.Zone} to PostgreSQL type 'timestamp with time zone', only UTC is supported. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); + } + + return 8; + } public int ValidateAndGetLength(OffsetDateTime value, NpgsqlParameter? parameter) - => 8; + { + if (!LegacyTimestampBehavior && value.Offset != Offset.Zero) + { + throw new InvalidCastException( + $"Cannot write OffsetDateTime with Offset={value.Offset} to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); + } + + return 8; + } public override void Write(Instant value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => WriteInstant(value, buf, _convertInfinityDateTime); + + internal static void WriteInstant(Instant value, NpgsqlWriteBuffer buf, bool convertInfinityDateTime) { - if (_convertInfinityDateTime) + if (convertInfinityDateTime) { if (value == Instant.MaxValue) { @@ -97,7 +98,8 @@ public override void Write(Instant value, NpgsqlWriteBuffer buf, NpgsqlParameter return; } } - TimestampHandler.WriteInteger(value, buf); + + buf.WriteInt64(EncodeInstant(value)); } void INpgsqlSimpleTypeHandler.Write(ZonedDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) @@ -106,24 +108,18 @@ void INpgsqlSimpleTypeHandler.Write(ZonedDateTime value, NpgsqlWr public void Write(OffsetDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => Write(value.ToInstant(), buf, parameter); - #endregion Write - - DateTimeOffset INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => _bclHandler.Read(buf, len, fieldDescription); - int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter) => _bclHandler.ValidateAndGetLength(value, parameter); void INpgsqlSimpleTypeHandler.Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => _bclHandler.Write(value, buf, parameter); - DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => _bclHandler.Read(buf, len, fieldDescription); - - int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) + int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) => _bclHandler.ValidateAndGetLength(value, parameter); - void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => _bclHandler.Write(value, buf, parameter); + + #endregion Write } } diff --git a/src/Npgsql.NodaTime/NpgsqlNodaTimeExtensions.cs b/src/Npgsql.NodaTime/NpgsqlNodaTimeExtensions.cs index 121ad50024..40ddea737b 100644 --- a/src/Npgsql.NodaTime/NpgsqlNodaTimeExtensions.cs +++ b/src/Npgsql.NodaTime/NpgsqlNodaTimeExtensions.cs @@ -1,10 +1,5 @@ -using System; -using System.Data; -using System.Runtime.CompilerServices; -using NodaTime; -using Npgsql.NodaTime.Internal; +using Npgsql.NodaTime.Internal; using Npgsql.TypeMapping; -using NpgsqlTypes; // ReSharper disable once CheckNamespace namespace Npgsql diff --git a/src/Npgsql.NodaTime/Properties/AssemblyInfo.cs b/src/Npgsql.NodaTime/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f4e3cc906f --- /dev/null +++ b/src/Npgsql.NodaTime/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Npgsql.NodaTime.Tests, PublicKey=" + +"0024000004800000940000000602000000240000525341310004000001000100" + +"2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + +"8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" + +"7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + +"29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] diff --git a/src/Npgsql.SourceGenerators/TypeHandlerSourceGenerator.cs b/src/Npgsql.SourceGenerators/TypeHandlerSourceGenerator.cs index 90e0bbfe02..9e4f6de269 100644 --- a/src/Npgsql.SourceGenerators/TypeHandlerSourceGenerator.cs +++ b/src/Npgsql.SourceGenerators/TypeHandlerSourceGenerator.cs @@ -57,7 +57,7 @@ void AugmentTypeHandler(INamedTypeSymbol typeSymbol, ClassDeclarationSyntax clas "System.Threading.Tasks", "Npgsql.Internal" }.Concat(classDeclarationSyntax.SyntaxTree.GetCompilationUnitRoot().Usings - .Where(u => u.Alias is null) + .Where(u => u.Alias is null && u.StaticKeyword.Kind() == SyntaxKind.None) .Select(u => u.Name.ToString()))); var interfaces = typeSymbol.AllInterfaces diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs new file mode 100644 index 0000000000..c59560f6d7 --- /dev/null +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs @@ -0,0 +1,121 @@ +using System; +using System.Runtime.CompilerServices; +using Npgsql.BackendMessages; +using NpgsqlTypes; + +namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers +{ + static class DateTimeUtils + { + const long PostgresTimestampOffsetTicks = 630822816000000000L; + const string InfinityExceptionMessage = "Can't convert infinite timestamp values to DateTime"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static DateTime DecodeTimestamp(long value, DateTimeKind kind) + => new(value * 10 + PostgresTimestampOffsetTicks, kind); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long EncodeTimestamp(DateTime value) + // Rounding here would cause problems because we would round up DateTime.MaxValue + // which would make it impossible to retrieve it back from the database, so we just drop the additional precision + => (value.Ticks - PostgresTimestampOffsetTicks) / 10; + + internal static DateTime ReadDateTime(NpgsqlReadBuffer buf, bool convertInfinityDateTime, DateTimeKind kind) + { + try + { + return buf.ReadInt64() switch + { + long.MaxValue => convertInfinityDateTime ? DateTime.MaxValue : throw new InvalidCastException(InfinityExceptionMessage), + long.MinValue => convertInfinityDateTime ? DateTime.MinValue : throw new InvalidCastException(InfinityExceptionMessage), + var value => DecodeTimestamp(value, kind) + }; + } + catch (ArgumentOutOfRangeException e) + { + throw new InvalidCastException("Out of the range of DateTime (year must be between 1 and 9999)", e); + } + } + + internal static NpgsqlDateTime ReadNpgsqlDateTime(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) + { + var value = buf.ReadInt64(); + if (value == long.MaxValue) + return NpgsqlDateTime.Infinity; + if (value == long.MinValue) + return NpgsqlDateTime.NegativeInfinity; + if (value >= 0) + { + var date = (int)(value / 86400000000L); + var time = value % 86400000000L; + + date += 730119; // 730119 = days since era (0001-01-01) for 2000-01-01 + time *= 10; // To 100ns + + return new NpgsqlDateTime(new NpgsqlDate(date), new TimeSpan(time)); + } + else + { + value = -value; + var date = (int)(value / 86400000000L); + var time = value % 86400000000L; + if (time != 0) + { + ++date; + time = 86400000000L - time; + } + + date = 730119 - date; // 730119 = days since era (0001-01-01) for 2000-01-01 + time *= 10; // To 100ns + + return new NpgsqlDateTime(new NpgsqlDate(date), new TimeSpan(time)); + } + } + + internal static void WriteTimestamp(DateTime value, NpgsqlWriteBuffer buf, bool convertInfinityDateTime) + { + if (value == DateTime.MaxValue && convertInfinityDateTime) + { + buf.WriteInt64(long.MaxValue); + return; + } + + if (value == DateTime.MinValue && convertInfinityDateTime) + { + buf.WriteInt64(long.MinValue); + return; + } + + var postgresTimestamp = EncodeTimestamp(value); + buf.WriteInt64(postgresTimestamp); + } + + internal static void WriteTimestamp(NpgsqlDateTime value, NpgsqlWriteBuffer buf, bool convertInfinityDateTime) + { + if (value.IsInfinity) + { + buf.WriteInt64(long.MaxValue); + return; + } + + if (value.IsNegativeInfinity) + { + buf.WriteInt64(long.MinValue); + return; + } + + var uSecsTime = value.Time.Ticks / 10; + + if (value >= new NpgsqlDateTime(2000, 1, 1, 0, 0, 0)) + { + var uSecsDate = (value.Date.DaysSinceEra - 730119) * 86400000000L; + buf.WriteInt64(uSecsDate + uSecsTime); + } + else + { + var uSecsDate = (730119 - value.Date.DaysSinceEra) * 86400000000L; + buf.WriteInt64(-(uSecsDate - uSecsTime)); + } + } + } +} diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimeTzHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimeTzHandler.cs index 3e815f0284..e6fa45fb27 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimeTzHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimeTzHandler.cs @@ -2,8 +2,6 @@ using Npgsql.BackendMessages; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; -using Npgsql.TypeMapping; -using NpgsqlTypes; namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers { @@ -17,7 +15,7 @@ namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers /// should be considered somewhat unstable, and may change in breaking ways, including in non-major releases. /// Use it at your own risk. /// - public partial class TimeTzHandler : NpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler, INpgsqlSimpleTypeHandler + public partial class TimeTzHandler : NpgsqlSimpleTypeHandler { // Binary Format: int64 expressing microseconds, int32 expressing timezone in seconds, negative @@ -37,22 +35,12 @@ public override DateTimeOffset Read(NpgsqlReadBuffer buf, int len, FieldDescript return new DateTimeOffset(ticks + TimeSpan.TicksPerDay, offset); } - DateTime INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => Read(buf, len, fieldDescription).LocalDateTime; - - TimeSpan INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) - => Read(buf, len, fieldDescription).LocalDateTime.TimeOfDay; - #endregion Read #region Write /// public override int ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter) => 12; - /// - public int ValidateAndGetLength(TimeSpan value, NpgsqlParameter? parameter) => 12; - /// - public int ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) => 12; /// public override void Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) @@ -61,33 +49,6 @@ public override void Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlPa buf.WriteInt32(-(int)(value.Offset.Ticks / TimeSpan.TicksPerSecond)); } - /// - public void Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - { - buf.WriteInt64(value.TimeOfDay.Ticks / 10); - - switch (value.Kind) - { - case DateTimeKind.Utc: - buf.WriteInt32(0); - break; - case DateTimeKind.Unspecified: - // Treat as local... - case DateTimeKind.Local: - buf.WriteInt32(-(int)(TimeZoneInfo.Local.BaseUtcOffset.Ticks / TimeSpan.TicksPerSecond)); - break; - default: - throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {value.Kind} of enum {nameof(DateTimeKind)}. Please file a bug."); - } - } - - /// - public void Write(TimeSpan value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - { - buf.WriteInt64(value.Ticks / 10); - buf.WriteInt32(-(int)(TimeZoneInfo.Local.BaseUtcOffset.Ticks / TimeSpan.TicksPerSecond)); - } - #endregion Write } } diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs index 68a24bace5..5dc07dddf9 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs @@ -1,11 +1,10 @@ using System; -using System.Data; -using System.Runtime.CompilerServices; using Npgsql.BackendMessages; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; -using Npgsql.TypeMapping; using NpgsqlTypes; +using static Npgsql.Util.Statics; +using static Npgsql.Internal.TypeHandlers.DateTimeHandlers.DateTimeUtils; namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers { @@ -36,148 +35,52 @@ public TimestampHandler(PostgresType postgresType, bool convertInfinityDateTime) #region Read - private protected const string InfinityExceptionMessage = "Can't convert infinite timestamp values to DateTime"; - private protected const string OutOfRangeExceptionMessage = "Out of the range of DateTime (year must be between 1 and 9999)"; - /// public override DateTime Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) - { - - var postgresTimestamp = buf.ReadInt64(); - if (postgresTimestamp == long.MaxValue) - return ConvertInfinityDateTime - ? DateTime.MaxValue - : throw new InvalidCastException(InfinityExceptionMessage); - if (postgresTimestamp == long.MinValue) - return ConvertInfinityDateTime - ? DateTime.MinValue - : throw new InvalidCastException(InfinityExceptionMessage); - - try - { - return FromPostgresTimestamp(postgresTimestamp); - } - catch (ArgumentOutOfRangeException e) - { - throw new InvalidCastException(OutOfRangeExceptionMessage, e); - } - } + => ReadDateTime(buf, ConvertInfinityDateTime, DateTimeKind.Unspecified); /// protected override NpgsqlDateTime ReadPsv(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) - => ReadTimeStamp(buf, len, fieldDescription); - - /// - /// Reads a timestamp from the buffer as an . - /// - protected NpgsqlDateTime ReadTimeStamp(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) - { - var value = buf.ReadInt64(); - if (value == long.MaxValue) - return NpgsqlDateTime.Infinity; - if (value == long.MinValue) - return NpgsqlDateTime.NegativeInfinity; - if (value >= 0) - { - var date = (int)(value / 86400000000L); - var time = value % 86400000000L; - - date += 730119; // 730119 = days since era (0001-01-01) for 2000-01-01 - time *= 10; // To 100ns - - return new NpgsqlDateTime(new NpgsqlDate(date), new TimeSpan(time)); - } - else - { - value = -value; - var date = (int)(value / 86400000000L); - var time = value % 86400000000L; - if (time != 0) - { - ++date; - time = 86400000000L - time; - } - - date = 730119 - date; // 730119 = days since era (0001-01-01) for 2000-01-01 - time *= 10; // To 100ns - - return new NpgsqlDateTime(new NpgsqlDate(date), new TimeSpan(time)); - } - } + => ReadNpgsqlDateTime(buf, len, fieldDescription); #endregion Read #region Write /// - public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) => 8; - - /// - public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? parameter) => 8; - - /// - public override void Write(NpgsqlDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) { - if (value.IsInfinity) + if (!LegacyTimestampBehavior && value.Kind == DateTimeKind.Utc) { - buf.WriteInt64(long.MaxValue); - return; + throw new InvalidCastException( + "Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } - if (value.IsNegativeInfinity) - { - buf.WriteInt64(long.MinValue); - return; - } - - var uSecsTime = value.Time.Ticks / 10; - - if (value >= new NpgsqlDateTime(2000, 1, 1, 0, 0, 0)) - { - var uSecsDate = (value.Date.DaysSinceEra - 730119) * 86400000000L; - buf.WriteInt64(uSecsDate + uSecsTime); - } - else - { - var uSecsDate = (730119 - value.Date.DaysSinceEra) * 86400000000L; - buf.WriteInt64(-(uSecsDate - uSecsTime)); - } + return 8; } /// - public override void Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? parameter) { - if (ConvertInfinityDateTime) + if (!LegacyTimestampBehavior && value.Kind == DateTimeKind.Utc) { - if (value == DateTime.MaxValue) - { - buf.WriteInt64(long.MaxValue); - return; - } - - if (value == DateTime.MinValue) - { - buf.WriteInt64(long.MinValue); - return; - } + throw new InvalidCastException( + "Cannot write NpgsqlDateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } - var postgresTimestamp = ToPostgresTimestamp(value); - buf.WriteInt64(postgresTimestamp); + return 8; } - #endregion Write - - const long PostgresTimestampOffsetTicks = 630822816000000000L; + /// + public override void Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => WriteTimestamp(value, buf, ConvertInfinityDateTime); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static long ToPostgresTimestamp(DateTime value) - // Rounding here would cause problems because we would round up DateTime.MaxValue - // which would make it impossible to retrieve it back from the database, so we just drop the additional precision - => (value.Ticks - PostgresTimestampOffsetTicks) / 10; + /// + public override void Write(NpgsqlDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + => WriteTimestamp(value, buf, ConvertInfinityDateTime); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static DateTime FromPostgresTimestamp(long value) - => new(value * 10 + PostgresTimestampOffsetTicks); + #endregion Write } } diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs index 10f6efea0e..b90aacab46 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs @@ -1,10 +1,12 @@ using System; -using System.Data; +using System.Diagnostics; +using System.Net.NetworkInformation; using Npgsql.BackendMessages; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; -using Npgsql.TypeMapping; using NpgsqlTypes; +using static Npgsql.Util.Statics; +using static Npgsql.Internal.TypeHandlers.DateTimeHandlers.DateTimeUtils; namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers { @@ -18,13 +20,20 @@ namespace Npgsql.Internal.TypeHandlers.DateTimeHandlers /// should be considered somewhat unstable, and may change in breaking ways, including in non-major releases. /// Use it at your own risk. /// - public partial class TimestampTzHandler : TimestampHandler, INpgsqlSimpleTypeHandler + public partial class TimestampTzHandler : NpgsqlSimpleTypeHandlerWithPsv, INpgsqlSimpleTypeHandler { + /// + /// Whether to convert positive and negative infinity values to DateTime.{Max,Min}Value when + /// a DateTime is requested + /// + protected readonly bool ConvertInfinityDateTime; + /// /// Constructs an . /// public TimestampTzHandler(PostgresType postgresType, bool convertInfinityDateTime) - : base(postgresType, convertInfinityDateTime) {} + : base(postgresType) + => ConvertInfinityDateTime = convertInfinityDateTime; /// public override NpgsqlTypeHandler CreateRangeHandler(PostgresType pgRangeType) @@ -32,54 +41,53 @@ public override NpgsqlTypeHandler CreateRangeHandler(PostgresType pgRangeType) #region Read + private protected const string InfinityExceptionMessage = "Can't convert infinite timestamp values to DateTime"; + private protected const string OutOfRangeExceptionMessage = "Out of the range of DateTime (year must be between 1 and 9999)"; + /// public override DateTime Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) { - var postgresTimestamp = buf.ReadInt64(); - if (postgresTimestamp == long.MaxValue) - return ConvertInfinityDateTime - ? DateTime.MaxValue - : throw new InvalidCastException(InfinityExceptionMessage); - if (postgresTimestamp == long.MinValue) - return ConvertInfinityDateTime - ? DateTime.MinValue - : throw new InvalidCastException(InfinityExceptionMessage); - - try - { - return FromPostgresTimestamp(postgresTimestamp).ToLocalTime(); - } - catch (ArgumentOutOfRangeException e) - { - throw new InvalidCastException(OutOfRangeExceptionMessage, e); - } + var dateTime = ReadDateTime(buf, ConvertInfinityDateTime, DateTimeKind.Utc); + return LegacyTimestampBehavior && (!ConvertInfinityDateTime || dateTime != DateTime.MaxValue && dateTime != DateTime.MinValue) + ? dateTime.ToLocalTime() + : dateTime; } /// protected override NpgsqlDateTime ReadPsv(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null) { - var ts = ReadTimeStamp(buf, len, fieldDescription); - return ts.IsFinite ? new NpgsqlDateTime(ts.Date, ts.Time, DateTimeKind.Utc).ToLocalTime() : ts; + var ts = ReadNpgsqlDateTime(buf, len, fieldDescription); + + if (!ts.IsFinite) + return ts; + + var npgsqlDateTime = new NpgsqlDateTime(ts.Date, ts.Time, DateTimeKind.Utc); + return LegacyTimestampBehavior ? npgsqlDateTime.ToLocalTime() : npgsqlDateTime; } DateTimeOffset INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription) { - var postgresTimestamp = buf.ReadInt64(); - if (postgresTimestamp == long.MaxValue) - return ConvertInfinityDateTime - ? DateTimeOffset.MaxValue - : throw new InvalidCastException(InfinityExceptionMessage); - if (postgresTimestamp == long.MinValue) - return ConvertInfinityDateTime - ? DateTimeOffset.MinValue - : throw new InvalidCastException(InfinityExceptionMessage); try { - return FromPostgresTimestamp(postgresTimestamp).ToLocalTime(); + var value = buf.ReadInt64(); + switch (value) + { + case long.MaxValue: + return ConvertInfinityDateTime + ? DateTimeOffset.MaxValue + : throw new InvalidCastException(InfinityExceptionMessage); + case long.MinValue: + return ConvertInfinityDateTime + ? DateTimeOffset.MinValue + : throw new InvalidCastException(InfinityExceptionMessage); + default: + var dateTime = DecodeTimestamp(value, DateTimeKind.Utc); + return LegacyTimestampBehavior ? dateTime.ToLocalTime() : dateTime; + } } catch (ArgumentOutOfRangeException e) { - throw new InvalidCastException(OutOfRangeExceptionMessage, e); + throw new InvalidCastException("Out of the range of DateTime (year must be between 1 and 9999)", e); } } @@ -88,61 +96,101 @@ DateTimeOffset INpgsqlSimpleTypeHandler.Read(NpgsqlReadBuffer bu #region Write /// - public int ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter) => 8; + public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) + { + if (!LegacyTimestampBehavior && value.Kind != DateTimeKind.Utc && + (!ConvertInfinityDateTime || value != DateTime.MinValue && value != DateTime.MaxValue)) + { + throw new InvalidCastException( + $"Cannot write DateTime with Kind={value.Kind} to PostgreSQL type 'timestamp with time zone', only UTC is supported. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); + } + + return 8; + } /// - public override void Write(NpgsqlDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? parameter) { - switch (value.Kind) + if (!LegacyTimestampBehavior && value.Kind != DateTimeKind.Utc && value.IsFinite) { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - break; - case DateTimeKind.Local: - value = value.ToUniversalTime(); - break; - default: - throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {value.Kind} of enum {nameof(DateTimeKind)}. Please file a bug."); + throw new InvalidCastException( + $"Cannot write DateTime with Kind={value.Kind} to PostgreSQL type 'timestamp with time zone', only UTC is supported. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } - base.Write(value, buf, parameter); + return 8; } /// - public override void Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + public int ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter) { - switch (value.Kind) + if (!LegacyTimestampBehavior && value.Offset != TimeSpan.Zero) { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - break; - case DateTimeKind.Local: - value = value.ToUniversalTime(); - break; - default: - throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {value.Kind} of enum {nameof(DateTimeKind)}. Please file a bug."); + throw new InvalidCastException( + $"Cannot write DateTimeOffset with Offset={value.Offset} to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. " + + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } - NpgsqlDateTime pgValue = value; - if (ConvertInfinityDateTime) + return 8; + } + + /// + public override void Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + { + if (LegacyTimestampBehavior) { - if (value == DateTime.MinValue) + switch (value.Kind) { - pgValue = NpgsqlDateTime.NegativeInfinity; + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + break; + case DateTimeKind.Local: + value = value.ToUniversalTime(); + break; + default: + throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {value.Kind} of enum {nameof(DateTimeKind)}. Please file a bug."); } - else if (value == DateTime.MaxValue) + } + else + Debug.Assert(value.Kind == DateTimeKind.Utc || (ConvertInfinityDateTime && (value == DateTime.MinValue || value == DateTime.MaxValue))); + + WriteTimestamp(value, buf, ConvertInfinityDateTime); + } + + /// + public override void Write(NpgsqlDateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) + { + if (LegacyTimestampBehavior) + { + switch (value.Kind) { - pgValue = NpgsqlDateTime.Infinity; + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + break; + case DateTimeKind.Local: + value = value.ToUniversalTime(); + break; + default: + throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {value.Kind} of enum {nameof(DateTimeKind)}. Please file a bug."); } } + else + Debug.Assert(value.Kind == DateTimeKind.Utc || !value.IsFinite); - // We cannot pass the DateTime value due to it implicitly converting to the NpgsqlDateTime anyway - base.Write(pgValue, buf, parameter); + WriteTimestamp(value, buf, ConvertInfinityDateTime); } /// public void Write(DateTimeOffset value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) - => base.Write(value.ToUniversalTime().DateTime, buf, parameter); + { + if (LegacyTimestampBehavior) + value = value.ToUniversalTime(); + + Debug.Assert(value.Offset == TimeSpan.Zero); + + WriteTimestamp(value.DateTime, buf, ConvertInfinityDateTime); + } #endregion Write } diff --git a/src/Npgsql/Internal/TypeHandling/ITypeHandlerResolver.cs b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs similarity index 69% rename from src/Npgsql/Internal/TypeHandling/ITypeHandlerResolver.cs rename to src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs index bc11850969..1b86b0f39d 100644 --- a/src/Npgsql/Internal/TypeHandling/ITypeHandlerResolver.cs +++ b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs @@ -5,23 +5,25 @@ namespace Npgsql.Internal.TypeHandling /// /// An Npgsql resolver for type handlers. Typically used by plugins to alter how Npgsql reads and writes values to PostgreSQL. /// - public interface ITypeHandlerResolver + public abstract class TypeHandlerResolver { /// /// Resolves a type handler given a PostgreSQL type name, corresponding to the typname column in the PostgreSQL pg_type catalog table. /// /// See . - NpgsqlTypeHandler? ResolveByDataTypeName(string typeName); + public abstract NpgsqlTypeHandler? ResolveByDataTypeName(string typeName); /// /// Resolves a type handler given a .NET CLR type. /// - NpgsqlTypeHandler? ResolveByClrType(Type type); + public abstract NpgsqlTypeHandler? ResolveByClrType(Type type); + + public virtual NpgsqlTypeHandler? ResolveValueDependentValue(object value) => null; /// /// Gets type mapping information for a given PostgreSQL type. /// Invoked in scenarios when mapping information is required, rather than a type handler for reading or writing. /// - TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName); + public abstract TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName); } } diff --git a/src/Npgsql/Internal/TypeHandling/ITypeHandlerResolverFactory.cs b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolverFactory.cs similarity index 58% rename from src/Npgsql/Internal/TypeHandling/ITypeHandlerResolverFactory.cs rename to src/Npgsql/Internal/TypeHandling/TypeHandlerResolverFactory.cs index 7a38d506eb..65132661a7 100644 --- a/src/Npgsql/Internal/TypeHandling/ITypeHandlerResolverFactory.cs +++ b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolverFactory.cs @@ -1,22 +1,25 @@ using System; using System.Data; -using Npgsql.Internal; using NpgsqlTypes; namespace Npgsql.Internal.TypeHandling { - public interface ITypeHandlerResolverFactory + public abstract class TypeHandlerResolverFactory { - ITypeHandlerResolver Create(NpgsqlConnector connector); + public abstract TypeHandlerResolver Create(NpgsqlConnector connector); - string? GetDataTypeNameByClrType(Type type); - TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName); + public abstract string? GetDataTypeNameByClrType(Type clrType); + public virtual string? GetDataTypeNameByValueDependentValue(object value) => null; + public abstract TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName); } static class TypeHandlerResolverFactoryExtensions { - internal static TypeMappingInfo? GetMappingByClrType(this ITypeHandlerResolverFactory factory, Type clrType) + internal static TypeMappingInfo? GetMappingByClrType(this TypeHandlerResolverFactory factory, Type clrType) => factory.GetDataTypeNameByClrType(clrType) is { } dataTypeName ? factory.GetMappingByDataTypeName(dataTypeName) : null; + + internal static TypeMappingInfo? GetMappingByValueDependentValue(this TypeHandlerResolverFactory factory, object value) + => factory.GetDataTypeNameByValueDependentValue(value) is { } dataTypeName ? factory.GetMappingByDataTypeName(dataTypeName) : null; } } diff --git a/src/Npgsql/NpgsqlParameter.cs b/src/Npgsql/NpgsqlParameter.cs index 1b7aad2714..ad2ba26510 100644 --- a/src/Npgsql/NpgsqlParameter.cs +++ b/src/Npgsql/NpgsqlParameter.cs @@ -11,6 +11,7 @@ using Npgsql.TypeMapping; using Npgsql.Util; using NpgsqlTypes; +using static Npgsql.Util.Statics; namespace Npgsql { @@ -313,7 +314,7 @@ public sealed override DbType DbType if (_value != null) // Infer from value but don't cache { - return GlobalTypeMapper.Instance.TryResolveMappingByClrType(_value.GetType(), out var mapping) + return GlobalTypeMapper.Instance.TryResolveMappingByValue(_value, out var mapping) ? mapping.DbType : DbType.Object; } @@ -347,7 +348,7 @@ public NpgsqlDbType NpgsqlDbType if (_value != null) // Infer from value { - return GlobalTypeMapper.Instance.TryResolveMappingByClrType(_value.GetType(), out var mapping) + return GlobalTypeMapper.Instance.TryResolveMappingByValue(_value, out var mapping) ? mapping.NpgsqlDbType ?? NpgsqlDbType.Unknown : throw new NotSupportedException("Can't infer NpgsqlDbType for type " + _value.GetType()); } @@ -382,7 +383,7 @@ public string? DataTypeName if (_value != null) // Infer from value { - return GlobalTypeMapper.Instance.TryResolveMappingByClrType(_value.GetType(), out var mapping) + return GlobalTypeMapper.Instance.TryResolveMappingByValue(_value, out var mapping) ? mapping.DataTypeName : null; } @@ -497,15 +498,15 @@ public sealed override string SourceColumn internal virtual void ResolveHandler(ConnectorTypeMapper typeMapper) { - if (Handler != null) + if (Handler is not null) return; if (_npgsqlDbType.HasValue) Handler = typeMapper.ResolveByNpgsqlDbType(_npgsqlDbType.Value); - else if (_dataTypeName != null) + else if (_dataTypeName is not null) Handler = typeMapper.ResolveByDataTypeName(_dataTypeName); - else if (_value != null) - Handler = typeMapper.ResolveByClrType(_value.GetType()); + else if (_value is not null) + Handler = typeMapper.ResolveByValue(_value); else throw new InvalidOperationException($"Parameter '{ParameterName}' must have its value set"); } diff --git a/src/Npgsql/NpgsqlParameter`.cs b/src/Npgsql/NpgsqlParameter`.cs index 11675bf96f..28ba95cc3f 100644 --- a/src/Npgsql/NpgsqlParameter`.cs +++ b/src/Npgsql/NpgsqlParameter`.cs @@ -1,11 +1,11 @@ using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Npgsql.Internal; using Npgsql.TypeMapping; using NpgsqlTypes; +using static Npgsql.Util.Statics; namespace Npgsql { @@ -68,16 +68,16 @@ public NpgsqlParameter(string parameterName, DbType dbType) internal override void ResolveHandler(ConnectorTypeMapper typeMapper) { - if (Handler != null) + if (Handler is not null) return; // TODO: Better exceptions in case of cast failure etc. if (_npgsqlDbType.HasValue) Handler = typeMapper.ResolveByNpgsqlDbType(_npgsqlDbType.Value); - else if (_dataTypeName != null) + else if (_dataTypeName is not null) Handler = typeMapper.ResolveByDataTypeName(_dataTypeName); else - Handler = typeMapper.ResolveByClrType(typeof(T)); + Handler = typeMapper.ResolveByValue(TypedValue); } internal override int ValidateAndGetLength() diff --git a/src/Npgsql/Properties/AssemblyInfo.cs b/src/Npgsql/Properties/AssemblyInfo.cs index f5d56bee72..a95cc5d548 100644 --- a/src/Npgsql/Properties/AssemblyInfo.cs +++ b/src/Npgsql/Properties/AssemblyInfo.cs @@ -29,6 +29,13 @@ "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] +[assembly: InternalsVisibleTo("Npgsql.NodaTime.Tests, PublicKey=" + +"0024000004800000940000000602000000240000525341310004000001000100" + +"2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + +"8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" + +"7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + +"29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] + [assembly: InternalsVisibleTo("Npgsql.Benchmarks, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100" + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + diff --git a/src/Npgsql/PublicAPI.Unshipped.txt b/src/Npgsql/PublicAPI.Unshipped.txt index 7d0d17b7c5..adf3ab8414 100644 --- a/src/Npgsql/PublicAPI.Unshipped.txt +++ b/src/Npgsql/PublicAPI.Unshipped.txt @@ -113,7 +113,7 @@ Npgsql.Replication.PgOutput.PgOutputReplicationOptions.PgOutputReplicationOption *REMOVED*Npgsql.Replication.PgOutput.PgOutputReplicationOptions.PgOutputReplicationOptions(System.Collections.Generic.IEnumerable! publicationNames, ulong protocolVersion = 1, bool? binary = null, bool? streaming = null) -> void Npgsql.Replication.PgOutput.PgOutputReplicationOptions.PgOutputReplicationOptions(System.Collections.Generic.IEnumerable! publicationNames, ulong protocolVersion, bool? binary = null, bool? streaming = null, bool? messages = null) -> void *REMOVED*Npgsql.TypeMapping.INpgsqlTypeMapper.AddMapping(Npgsql.TypeMapping.NpgsqlTypeMapping! mapping) -> Npgsql.TypeMapping.INpgsqlTypeMapper! -Npgsql.TypeMapping.INpgsqlTypeMapper.AddTypeResolverFactory(Npgsql.Internal.TypeHandling.ITypeHandlerResolverFactory! resolverFactory) -> void +Npgsql.TypeMapping.INpgsqlTypeMapper.AddTypeResolverFactory(Npgsql.Internal.TypeHandling.TypeHandlerResolverFactory! resolverFactory) -> void *REMOVED*Npgsql.TypeMapping.INpgsqlTypeMapper.Mappings.get -> System.Collections.Generic.IEnumerable! *REMOVED*Npgsql.TypeMapping.INpgsqlTypeMapper.RemoveMapping(string! pgTypeName) -> bool *REMOVED*Npgsql.TypeMapping.NpgsqlTypeMapping diff --git a/src/Npgsql/Replication/PgOutput/PgOutputAsyncEnumerable.cs b/src/Npgsql/Replication/PgOutput/PgOutputAsyncEnumerable.cs index ec20436a4a..54bf57fe60 100644 --- a/src/Npgsql/Replication/PgOutput/PgOutputAsyncEnumerable.cs +++ b/src/Npgsql/Replication/PgOutput/PgOutputAsyncEnumerable.cs @@ -88,7 +88,7 @@ async IAsyncEnumerator StartReplicationInternal(Canc await buf.EnsureAsync(20); yield return _beginMessage.Populate(xLogData.WalStart, xLogData.WalEnd, xLogData.ServerClock, transactionFinalLsn: new NpgsqlLogSequenceNumber(buf.ReadUInt64()), - transactionCommitTimestamp: TimestampHandler.FromPostgresTimestamp(buf.ReadInt64()), + transactionCommitTimestamp: DateTimeUtils.DecodeTimestamp(buf.ReadInt64(), DateTimeKind.Unspecified), transactionXid: buf.ReadUInt32()); continue; } @@ -123,7 +123,7 @@ async IAsyncEnumerator StartReplicationInternal(Canc yield return _commitMessage.Populate(xLogData.WalStart, xLogData.WalEnd, xLogData.ServerClock, buf.ReadByte(), commitLsn: new NpgsqlLogSequenceNumber(buf.ReadUInt64()), transactionEndLsn: new NpgsqlLogSequenceNumber(buf.ReadUInt64()), - transactionCommitTimestamp: TimestampHandler.FromPostgresTimestamp(buf.ReadInt64())); + transactionCommitTimestamp: DateTimeUtils.DecodeTimestamp(buf.ReadInt64(), DateTimeKind.Unspecified)); continue; } case BackendReplicationMessageCode.Origin: @@ -342,7 +342,7 @@ async IAsyncEnumerator StartReplicationInternal(Canc yield return _streamCommitMessage.Populate(xLogData.WalStart, xLogData.WalEnd, xLogData.ServerClock, transactionXid: buf.ReadUInt32(), flags: buf.ReadByte(), commitLsn: new NpgsqlLogSequenceNumber(buf.ReadUInt64()), transactionEndLsn: new NpgsqlLogSequenceNumber(buf.ReadUInt64()), - transactionCommitTimestamp: TimestampHandler.FromPostgresTimestamp(buf.ReadInt64())); + transactionCommitTimestamp: DateTimeUtils.DecodeTimestamp(buf.ReadInt64(), DateTimeKind.Unspecified)); continue; } case BackendReplicationMessageCode.StreamAbort: diff --git a/src/Npgsql/Replication/ReplicationConnection.cs b/src/Npgsql/Replication/ReplicationConnection.cs index afc996a870..87bb8424e9 100644 --- a/src/Npgsql/Replication/ReplicationConnection.cs +++ b/src/Npgsql/Replication/ReplicationConnection.cs @@ -516,7 +516,7 @@ internal async IAsyncEnumerator StartReplicationInternal( await buf.EnsureAsync(24); var startLsn = buf.ReadUInt64(); var endLsn = buf.ReadUInt64(); - var sendTime = TimestampHandler.FromPostgresTimestamp(buf.ReadInt64()).ToLocalTime(); + var sendTime = DateTimeUtils.DecodeTimestamp(buf.ReadInt64(), DateTimeKind.Unspecified).ToLocalTime(); if (unchecked((ulong)Interlocked.Read(ref _lastReceivedLsn)) < startLsn) Interlocked.Exchange(ref _lastReceivedLsn, unchecked((long)startLsn)); @@ -547,7 +547,7 @@ internal async IAsyncEnumerator StartReplicationInternal( if (Log.IsEnabled(NpgsqlLogLevel.Trace)) { var endLsn = new NpgsqlLogSequenceNumber(end); - var timestamp = TimestampHandler.FromPostgresTimestamp(buf.ReadInt64()).ToLocalTime(); + var timestamp = DateTimeUtils.DecodeTimestamp(buf.ReadInt64(), DateTimeKind.Unspecified).ToLocalTime(); Log.Trace($"Received replication primary keepalive message from the server with current end of WAL of {endLsn} and timestamp of {timestamp}...", Connector.Id); } else @@ -691,7 +691,7 @@ async Task SendFeedback(bool waitOnSemaphore = false, bool requestReply = false, buf.WriteInt64(lastReceivedLsn); buf.WriteInt64(lastFlushedLsn); buf.WriteInt64(lastAppliedLsn); - buf.WriteInt64(TimestampHandler.ToPostgresTimestamp(timestamp)); + buf.WriteInt64(DateTimeUtils.EncodeTimestamp(timestamp)); buf.WriteByte(requestReply ? (byte)1 : (byte)0); await connector.Flush(async: true, cancellationToken); diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs index 9ec757a017..6e0b1d03d5 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs @@ -21,10 +21,11 @@ using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using NpgsqlTypes; +using static Npgsql.Util.Statics; namespace Npgsql.TypeMapping { - class BuiltInTypeHandlerResolver : ITypeHandlerResolver + class BuiltInTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlConnector _connector; readonly NpgsqlDatabaseInfo _databaseInfo; @@ -58,11 +59,11 @@ class BuiltInTypeHandlerResolver : ITypeHandlerResolver { "jsonpath", new(NpgsqlDbType.JsonPath, DbType.Object, "jsonpath") }, // Date/time types - { "timestamp without time zone", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(NpgsqlDateTime), typeof(DateTime)) }, - { "timestamp", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(DateTimeOffset)) }, - { "timestamp with time zone", new(NpgsqlDbType.TimestampTz, DbType.DateTime, "timestamp with time zone", typeof(DateTimeOffset)) }, - { "timestamptz", new(NpgsqlDbType.TimestampTz, DbType.DateTime, "timestamp with time zone", typeof(DateTimeOffset)) }, - { "date", new(NpgsqlDbType.Date, DbType.Date, "date", typeof(NpgsqlDate) + { "timestamp without time zone", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(NpgsqlDateTime), typeof(DateTime)) }, + { "timestamp", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(DateTimeOffset)) }, + { "timestamp with time zone", new(NpgsqlDbType.TimestampTz, DbType.DateTimeOffset, "timestamp with time zone", typeof(DateTimeOffset)) }, + { "timestamptz", new(NpgsqlDbType.TimestampTz, DbType.DateTimeOffset, "timestamp with time zone", typeof(DateTimeOffset)) }, + { "date", new(NpgsqlDbType.Date, DbType.Date, "date", typeof(NpgsqlDate) #if NET6_0_OR_GREATER , typeof(DateOnly) #endif @@ -81,6 +82,13 @@ class BuiltInTypeHandlerResolver : ITypeHandlerResolver { "timetz", new(NpgsqlDbType.TimeTz, DbType.Object, "time with time zone") }, { "interval", new(NpgsqlDbType.Interval, DbType.Object, "interval", typeof(TimeSpan), typeof(NpgsqlTimeSpan)) }, + { "timestamp without time zone[]", new(NpgsqlDbType.Array | NpgsqlDbType.Timestamp, DbType.Object, "timestamp without time zone[]") }, + { "timestamp with time zone[]", new(NpgsqlDbType.Array | NpgsqlDbType.TimestampTz, DbType.Object, "timestamp with time zone[]") }, + { "tsrange", new(NpgsqlDbType.Range | NpgsqlDbType.Timestamp, DbType.Object, "tsrange") }, + { "tstzrange", new(NpgsqlDbType.Range | NpgsqlDbType.TimestampTz, DbType.Object, "tstzrange") }, + { "tsmultirange", new(NpgsqlDbType.Multirange | NpgsqlDbType.Timestamp, DbType.Object, "tsmultirange") }, + { "tstzmultirange", new(NpgsqlDbType.Multirange | NpgsqlDbType.TimestampTz, DbType.Object, "tstzmultirange") }, + // Network types { "cidr", new(NpgsqlDbType.Cidr, DbType.Object, "cidr") }, #pragma warning disable 618 @@ -230,6 +238,14 @@ class BuiltInTypeHandlerResolver : ITypeHandlerResolver // Special types UnknownTypeHandler? _unknownHandler; + // Complex type handlers over timestamp/timestamptz (because DateTime is value-dependent) + NpgsqlTypeHandler? _timestampArrayHandler; + NpgsqlTypeHandler? _timestampTzArrayHandler; + NpgsqlTypeHandler? _timestampRangeHandler; + NpgsqlTypeHandler? _timestampTzRangeHandler; + NpgsqlTypeHandler? _timestampMultirangeHandler; + NpgsqlTypeHandler? _timestampTzMultirangeHandler; + #endregion Cached handlers internal BuiltInTypeHandlerResolver(NpgsqlConnector connector) @@ -252,7 +268,7 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector) _jsonbHandler ??= new JsonHandler(PgType("jsonb"), _connector.TextEncoding, isJsonb: true); } - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName switch { // Numeric types @@ -338,90 +354,179 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector) _ => null }; - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => ClrTypeToDataTypeNameTable.TryGetValue(type, out var dataTypeName) && ResolveByDataTypeName(dataTypeName) is { } handler ? handler : null; - static readonly Dictionary ClrTypeToDataTypeNameTable = new() + static readonly Dictionary ClrTypeToDataTypeNameTable; + + static BuiltInTypeHandlerResolver() { - // Numeric types - { typeof(byte), "smallint" }, - { typeof(short), "smallint" }, - { typeof(int), "integer" }, - { typeof(long), "bigint" }, - { typeof(float), "real" }, - { typeof(double), "double precision" }, - { typeof(decimal), "decimal" }, - { typeof(BigInteger), "decimal" }, + ClrTypeToDataTypeNameTable = new() + { + // Numeric types + { typeof(byte), "smallint" }, + { typeof(short), "smallint" }, + { typeof(int), "integer" }, + { typeof(long), "bigint" }, + { typeof(float), "real" }, + { typeof(double), "double precision" }, + { typeof(decimal), "decimal" }, + { typeof(BigInteger), "decimal" }, - // Text types - { typeof(string), "text" }, - { typeof(char[]), "text" }, - { typeof(char), "text" }, - { typeof(ArraySegment), "text" }, - { typeof(JsonDocument), "jsonb" }, + // Text types + { typeof(string), "text" }, + { typeof(char[]), "text" }, + { typeof(char), "text" }, + { typeof(ArraySegment), "text" }, + { typeof(JsonDocument), "jsonb" }, - // Date/time types - { typeof(DateTime), "timestamp without time zone" }, - { typeof(NpgsqlDateTime), "timestamp without time zone" }, - { typeof(DateTimeOffset), "timestamp with time zone" }, - { typeof(NpgsqlDate), "date" }, + // Date/time types + // The DateTime entry is for LegacyTimestampBehavior mode only. In regular mode we resolve through + // ResolveValueDependentValue below + { typeof(DateTime), "timestamp without time zone" }, + { typeof(NpgsqlDateTime), "timestamp without time zone" }, + { typeof(DateTimeOffset), "timestamp with time zone" }, + { typeof(NpgsqlDate), "date" }, #if NET6_0_OR_GREATER - { typeof(DateOnly), "date" }, - { typeof(TimeOnly), "time without time zone" }, + { typeof(DateOnly), "date" }, + { typeof(TimeOnly), "time without time zone" }, #endif - { typeof(TimeSpan), "interval" }, - { typeof(NpgsqlTimeSpan), "interval" }, + { typeof(TimeSpan), "interval" }, + { typeof(NpgsqlTimeSpan), "interval" }, - // Network types - { typeof(IPAddress), "inet" }, - { ReadonlyIpType, "inet" }, - { typeof((IPAddress Address, int Subnet)), "inet" }, + // Network types + { typeof(IPAddress), "inet" }, + { ReadonlyIpType, "inet" }, + { typeof((IPAddress Address, int Subnet)), "inet" }, #pragma warning disable 618 - { typeof(NpgsqlInet), "inet" }, + { typeof(NpgsqlInet), "inet" }, #pragma warning restore 618 - { typeof(PhysicalAddress), "macaddr" }, + { typeof(PhysicalAddress), "macaddr" }, - // Full-text types - { typeof(NpgsqlTsQuery), "tsquery" }, - { typeof(NpgsqlTsVector), "tsvector" }, + // Full-text types + { typeof(NpgsqlTsQuery), "tsquery" }, + { typeof(NpgsqlTsVector), "tsvector" }, - // Geometry types - { typeof(NpgsqlBox), "box" }, - { typeof(NpgsqlCircle), "circle" }, - { typeof(NpgsqlLine), "line" }, - { typeof(NpgsqlLSeg), "lseg" }, - { typeof(NpgsqlPath), "path" }, - { typeof(NpgsqlPoint), "point" }, - { typeof(NpgsqlPolygon), "polygon" }, + // Geometry types + { typeof(NpgsqlBox), "box" }, + { typeof(NpgsqlCircle), "circle" }, + { typeof(NpgsqlLine), "line" }, + { typeof(NpgsqlLSeg), "lseg" }, + { typeof(NpgsqlPath), "path" }, + { typeof(NpgsqlPoint), "point" }, + { typeof(NpgsqlPolygon), "polygon" }, - // Misc types - { typeof(bool), "boolean" }, - { typeof(byte[]), "bytea" }, - { typeof(ArraySegment), "bytea" }, + // Misc types + { typeof(bool), "boolean" }, + { typeof(byte[]), "bytea" }, + { typeof(ArraySegment), "bytea" }, #if !NETSTANDARD2_0 - { typeof(ReadOnlyMemory), "bytea" }, - { typeof(Memory), "bytea" }, + { typeof(ReadOnlyMemory), "bytea" }, + { typeof(Memory), "bytea" }, #endif - { typeof(Guid), "uuid" }, - { typeof(BitArray), "bit varying" }, - { typeof(BitVector32), "bit varying" }, - { typeof(Dictionary), "hstore" }, + { typeof(Guid), "uuid" }, + { typeof(BitArray), "bit varying" }, + { typeof(BitVector32), "bit varying" }, + { typeof(Dictionary), "hstore" }, #if !NETSTANDARD2_0 && !NETSTANDARD2_1 - { typeof(ImmutableDictionary), "hstore" }, + { typeof(ImmutableDictionary), "hstore" }, #endif - // Internal types - { typeof(NpgsqlLogSequenceNumber), "pg_lsn" }, - { typeof(NpgsqlTid), "tid" }, - { typeof(DBNull), "unknown" } - }; + // Internal types + { typeof(NpgsqlLogSequenceNumber), "pg_lsn" }, + { typeof(NpgsqlTid), "tid" }, + { typeof(DBNull), "unknown" } + }; + + if (LegacyTimestampBehavior) + ClrTypeToDataTypeNameTable[typeof(DateTime)] = "timestamp without time zone"; + } + + public override NpgsqlTypeHandler? ResolveValueDependentValue(object value) + { + // In LegacyTimestampBehavior, DateTime isn't value-dependent, and handled above in ClrTypeToDataTypeNameTable like other types + if (LegacyTimestampBehavior) + return null; + + return value switch + { + DateTime dateTime => dateTime.Kind == DateTimeKind.Utc ? _timestampTzHandler : _timestampHandler, + + // For arrays/lists, return timestamp or timestamptz based on the kind of the first DateTime; if the user attempts to + // mix incompatible Kinds, that will fail during validation. For empty arrays it doesn't matter. + IList array => ArrayHandler(array.Count == 0 ? DateTimeKind.Unspecified : array[0].Kind), + + NpgsqlRange range => RangeHandler(!range.LowerBoundInfinite ? range.LowerBound.Kind : + !range.UpperBoundInfinite ? range.UpperBound.Kind : DateTimeKind.Unspecified), + + NpgsqlRange[] multirange => MultirangeHandler(GetMultirangeKind(multirange)), + _ => null + }; + + NpgsqlTypeHandler ArrayHandler(DateTimeKind kind) + => kind == DateTimeKind.Utc + ? _timestampTzArrayHandler ??= _timestampTzHandler.CreateArrayHandler( + (PostgresArrayType)PgType("timestamp with time zone[]"), _connector.Settings.ArrayNullabilityMode) + : _timestampArrayHandler ??= _timestampHandler.CreateArrayHandler( + (PostgresArrayType)PgType("timestamp without time zone[]"), _connector.Settings.ArrayNullabilityMode); + + NpgsqlTypeHandler RangeHandler(DateTimeKind kind) + => kind == DateTimeKind.Utc + ? _timestampTzRangeHandler ??= _timestampTzHandler.CreateRangeHandler((PostgresRangeType)PgType("tstzrange")) + : _timestampRangeHandler ??= _timestampHandler.CreateRangeHandler((PostgresRangeType)PgType("tsrange")); + + NpgsqlTypeHandler MultirangeHandler(DateTimeKind kind) + => kind == DateTimeKind.Utc + ? _timestampTzMultirangeHandler ??= _timestampTzHandler.CreateMultirangeHandler((PostgresMultirangeType)PgType("tstzmultirange")) + : _timestampMultirangeHandler ??= _timestampHandler.CreateMultirangeHandler((PostgresMultirangeType)PgType("tsmultirange")); + } + + static DateTimeKind GetRangeKind(NpgsqlRange range) + => !range.LowerBoundInfinite + ? range.LowerBound.Kind + : !range.UpperBoundInfinite + ? range.UpperBound.Kind + : DateTimeKind.Unspecified; + + static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) + { + for (var i = 0; i < multirange.Length; i++) + if (!multirange[i].IsEmpty) + return GetRangeKind(multirange[i]); + + return DateTimeKind.Unspecified; + } + + internal static string? ValueDependentValueToDataTypeName(object value) + { + // In LegacyTimestampBehavior, DateTime isn't value-dependent, and handled above in ClrTypeToDataTypeNameTable like other types + if (LegacyTimestampBehavior) + return null; + + return value switch + { + DateTime dateTime => dateTime.Kind == DateTimeKind.Utc ? "timestamp with time zone" : "timestamp without time zone", + + // For arrays/lists, return timestamp or timestamptz based on the kind of the first DateTime; if the user attempts to + // mix incompatible Kinds, that will fail during validation. For empty arrays it doesn't matter. + IList array => array.Count == 0 + ? "timestamp without time zone[]" + : array[0].Kind == DateTimeKind.Utc ? "timestamp with time zone[]" : "timestamp without time zone[]", + + NpgsqlRange range => GetRangeKind(range) == DateTimeKind.Utc ? "tstzrange" : "tsrange", + + NpgsqlRange[] multirange => GetMultirangeKind(multirange) == DateTimeKind.Utc ? "tstzmultirange" : "tsmultirange", + + _ => null + }; + } internal static string? ClrTypeToDataTypeName(Type type) => ClrTypeToDataTypeNameTable.TryGetValue(type, out var dataTypeName) ? dataTypeName : null; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => DoGetMappingByDataTypeName(dataTypeName); internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName) diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolverFactory.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolverFactory.cs index 278e1d5407..6f3917fc78 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolverFactory.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolverFactory.cs @@ -4,15 +4,18 @@ namespace Npgsql.TypeMapping { - class BuiltInTypeHandlerResolverFactory : ITypeHandlerResolverFactory + class BuiltInTypeHandlerResolverFactory : TypeHandlerResolverFactory { - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new BuiltInTypeHandlerResolver(connector); - public string? GetDataTypeNameByClrType(Type type) - => BuiltInTypeHandlerResolver.ClrTypeToDataTypeName(type); + public override string? GetDataTypeNameByClrType(Type clrType) + => BuiltInTypeHandlerResolver.ClrTypeToDataTypeName(clrType); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) + public override string? GetDataTypeNameByValueDependentValue(object value) + => BuiltInTypeHandlerResolver.ValueDependentValueToDataTypeName(value); + + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => BuiltInTypeHandlerResolver.DoGetMappingByDataTypeName(dataTypeName); } } diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs index 574d065872..e239444ebd 100644 --- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs +++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; @@ -32,7 +33,7 @@ internal NpgsqlDatabaseInfo DatabaseInfo } } - volatile ITypeHandlerResolver[] _resolvers; + volatile TypeHandlerResolver[] _resolvers; internal NpgsqlTypeHandler UnrecognizedTypeHandler { get; } readonly ConcurrentDictionary _handlersByOID = new(); @@ -55,7 +56,7 @@ internal ConnectorTypeMapper(NpgsqlConnector connector) : base(GlobalTypeMapper. { Connector = connector; UnrecognizedTypeHandler = new UnknownTypeHandler(Connector); - _resolvers = Array.Empty(); + _resolvers = Array.Empty(); } #endregion Constructors @@ -219,6 +220,38 @@ internal NpgsqlTypeHandler ResolveByDataTypeName(string typeName) } } + internal NpgsqlTypeHandler ResolveByValue(T value) + { + if (value is null) + return ResolveByClrType(typeof(T)); + + // TODO: do better + return ResolveByValue((object)value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal NpgsqlTypeHandler ResolveByValue(object value) + { + // We resolve as follows: + // 1. Cached by-type lookup (fast path). This will work for almost all types after the very first resolution. + // 2. Value-dependent type lookup (e.g. DateTime by Kind) via the resolvers. This includes complex types (e.g. array/range + // over DateTime), and the results cannot be cached. + // 3. Uncached by-type lookup (for the very first resolution of a given type) + + var type = value.GetType(); + if (_handlersByClrType.TryGetValue(type, out var handler)) + return handler; + + foreach (var resolver in _resolvers) + if ((handler = resolver.ResolveValueDependentValue(value)) is not null) + return handler; + + // ResolveByClrType either throws, or resolves a handler and caches it in _handlersByClrType (where it would be found above the + // next time we resolve this type) + return ResolveByClrType(value.GetType()); + } + + // TODO: This is needed as a separate method only because of binary COPY, see #3957 internal NpgsqlTypeHandler ResolveByClrType(Type type) { if (_handlersByClrType.TryGetValue(type, out var handler)) @@ -480,12 +513,12 @@ void ApplyUserMapping(PostgresType pgType, Type clrType, NpgsqlTypeHandler handl _userTypeMappings[pgType.OID] = new(npgsqlDbType: null, DbType.Object, pgType.Name, clrType); } - public override void AddTypeResolverFactory(ITypeHandlerResolverFactory resolverFactory) + public override void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory) { lock (this) { var oldResolvers = _resolvers; - var newResolvers = new ITypeHandlerResolver[oldResolvers.Length + 1]; + var newResolvers = new TypeHandlerResolver[oldResolvers.Length + 1]; Array.Copy(oldResolvers, 0, newResolvers, 1, oldResolvers.Length); newResolvers[0] = resolverFactory.Create(Connector); _resolvers = newResolvers; @@ -513,9 +546,10 @@ public override void Reset() _handlersByClrType.Clear(); _handlersByDataTypeName.Clear(); - _resolvers = new ITypeHandlerResolver[globalMapper.ResolverFactories.Count]; - for (var i = 0; i < _resolvers.Length; i++) - _resolvers[i] = globalMapper.ResolverFactories[i].Create(Connector); + var newResolvers = new TypeHandlerResolver[globalMapper.ResolverFactories.Count]; + for (var i = 0; i < newResolvers.Length; i++) + newResolvers[i] = globalMapper.ResolverFactories[i].Create(Connector); + _resolvers = newResolvers; _userTypeMappings.Clear(); diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs index 8e6753e7ed..77a8d913d3 100644 --- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs +++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs @@ -15,7 +15,7 @@ sealed class GlobalTypeMapper : TypeMapperBase { public static GlobalTypeMapper Instance { get; } - internal List ResolverFactories { get; } = new(); + internal List ResolverFactories { get; } = new(); internal Dictionary UserTypeMappings { get; } = new(); /// @@ -158,7 +158,7 @@ public override bool UnmapComposite(Type clrType, string? pgName = null, INpgsql } } - public override void AddTypeResolverFactory(ITypeHandlerResolverFactory resolverFactory) + public override void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory) { Lock.EnterWriteLock(); try @@ -197,10 +197,23 @@ public override void Reset() #region NpgsqlDbType/DbType inference for NpgsqlParameter [RequiresUnreferencedCodeAttribute("ToNpgsqlDbType uses interface-based reflection and isn't trimming-safe")] - internal bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out TypeMappingInfo? typeMapping) + internal bool TryResolveMappingByValue(object value, [NotNullWhen(true)] out TypeMappingInfo? typeMapping) { Lock.EnterReadLock(); try + { + foreach (var resolverFactory in ResolverFactories) + if ((typeMapping = resolverFactory.GetMappingByValueDependentValue(value)) is not null) + return true; + + return TryResolveMappingByClrType(value.GetType(), out typeMapping); + } + finally + { + Lock.ExitReadLock(); + } + + bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out TypeMappingInfo? typeMapping) { foreach (var resolverFactory in ResolverFactories) if ((typeMapping = resolverFactory.GetMappingByClrType(clrType)) is not null) @@ -257,10 +270,6 @@ internal bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out T throw new NotSupportedException("Can't infer NpgsqlDbType for type " + clrType); } - finally - { - Lock.ExitReadLock(); - } } #endregion NpgsqlDbType/DbType inference for NpgsqlParameter diff --git a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs index a35c4882d5..957d48020c 100644 --- a/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs +++ b/src/Npgsql/TypeMapping/INpgsqlTypeMapper.cs @@ -151,7 +151,7 @@ bool UnmapComposite( /// Typically used by plugins. /// /// The type resolver factory to be added. - void AddTypeResolverFactory(ITypeHandlerResolverFactory resolverFactory); + void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory); /// /// Resets all mapping changes performed on this type mapper and reverts it to its original, starting state. diff --git a/src/Npgsql/TypeMapping/TypeMapperBase.cs b/src/Npgsql/TypeMapping/TypeMapperBase.cs index 4e8259a17d..e32e909dae 100644 --- a/src/Npgsql/TypeMapping/TypeMapperBase.cs +++ b/src/Npgsql/TypeMapping/TypeMapperBase.cs @@ -43,7 +43,7 @@ public abstract bool UnmapEnum(string? pgName = null, INpgsqlNameTranslat public abstract bool UnmapComposite(Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null); /// - public abstract void AddTypeResolverFactory(ITypeHandlerResolverFactory resolverFactory); + public abstract void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory); public abstract void Reset(); diff --git a/src/Npgsql/Util/PGUtil.cs b/src/Npgsql/Util/PGUtil.cs index c301bad706..7782444bc1 100644 --- a/src/Npgsql/Util/PGUtil.cs +++ b/src/Npgsql/Util/PGUtil.cs @@ -11,6 +11,15 @@ namespace Npgsql.Util { static class Statics { +#if DEBUG + internal static bool LegacyTimestampBehavior; +#else + internal static readonly bool LegacyTimestampBehavior; +#endif + + static Statics() + => LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static T Expect(IBackendMessage msg, NpgsqlConnector connector) { diff --git a/test/Npgsql.NodaTime.Tests/LegacyNodaTimeTests.cs b/test/Npgsql.NodaTime.Tests/LegacyNodaTimeTests.cs new file mode 100644 index 0000000000..cb8c394a60 --- /dev/null +++ b/test/Npgsql.NodaTime.Tests/LegacyNodaTimeTests.cs @@ -0,0 +1,325 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using NodaTime; +using Npgsql.Tests; +using NpgsqlTypes; +using NUnit.Framework; + +namespace Npgsql.NodaTime.Tests +{ + [NonParallelizable] + public class LegacyNodaTimeTests : TestBase + { + static readonly TestCaseData[] TimestampValues = + { + new TestCaseData(new LocalDateTime(1998, 4, 12, 13, 26, 38, 789), "1998-04-12 13:26:38.789") + .SetName("TimestampPre2000"), + new TestCaseData(new LocalDateTime(2015, 1, 27, 8, 45, 12, 345), "2015-01-27 08:45:12.345") + .SetName("TimestampPost2000"), + new TestCaseData(new LocalDateTime(1999, 12, 31, 23, 59, 59, 999).PlusNanoseconds(456000), "1999-12-31 23:59:59.999456") + .SetName("TimestampMicroseconds"), + }; + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_read(LocalDateTime localDateTime, string s) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}'::timestamp without time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp without time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Instant))); + + Assert.That(reader[0], Is.EqualTo(localDateTime.InUtc().ToInstant())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDateTime.InUtc().ToInstant())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDateTime)); + Assert.That(reader.GetDateTime(0), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); + + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetDate(0), Throws.TypeOf()); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_write_values(LocalDateTime localDateTime, string expected) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = + { + new() { Value = localDateTime, NpgsqlDbType = NpgsqlDbType.Timestamp } + } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestampParameters + { + get + { + var localDateTime = new LocalDateTime(1998, 4, 12, 13, 26, 38); + + return new NpgsqlParameter[] + { + new() { Value = localDateTime }, + new() { Value = localDateTime.InUtc().ToInstant() }, + new() { Value = localDateTime, NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = localDateTime, DbType = DbType.DateTime }, + new() { Value = localDateTime, DbType = DbType.DateTime2 }, + new() { Value = localDateTime.ToDateTimeUnspecified() }, + }; + } + } + + [Test, TestCaseSource(nameof(TimestampParameters))] + public async Task Timestamp_resolution(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + conn.TypeMapper.UseNodaTime(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp without time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 13:26:38")); + } + + [Test] + public async Task Timestamp_read_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = + new NpgsqlCommand("SELECT 'infinity'::timestamp without time zone, '-infinity'::timestamp without time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetFieldValue(0), Is.EqualTo(Instant.MaxValue)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(DateTime.MaxValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(Instant.MinValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(DateTime.MinValue)); + } + + [Test] + public async Task Timestamp_write_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = new NpgsqlCommand("SELECT $1::text, $2::text, $3::text, $4::text", conn) + { + Parameters = + { + new() { Value = Instant.MaxValue }, + new() { Value = DateTime.MaxValue }, + new() { Value = Instant.MinValue }, + new() { Value = DateTime.MinValue } + } + }; + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader[0], Is.EqualTo("infinity")); + Assert.That(reader[1], Is.EqualTo("infinity")); + Assert.That(reader[2], Is.EqualTo("-infinity")); + Assert.That(reader[3], Is.EqualTo("-infinity")); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamptz_read(LocalDateTime expectedLocalDateTime, string s) + { + var expectedInstance = expectedLocalDateTime.InUtc().ToInstant(); + + await using var conn = await OpenConnectionAsync(); + var timezone = "America/New_York"; + await conn.ExecuteNonQueryAsync($"SET TIMEZONE TO '{timezone}'"); + + await using var cmd = new NpgsqlCommand($"SELECT '{s}+00'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp with time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Instant))); + + Assert.That(reader[0], Is.EqualTo(expectedInstance)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.InZone(DateTimeZoneProviders.Tzdb[timezone]))); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.InZone(DateTimeZoneProviders.Tzdb[timezone]).ToOffsetDateTime())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.ToDateTimeUtc().ToLocalTime())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.ToDateTimeOffset().ToLocalTime())); + + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetDate(0), Throws.TypeOf()); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamptz_write_values(LocalDateTime localDateTime, string expected) + { + await using var conn = await OpenConnectionAsync(); + await conn.ExecuteNonQueryAsync("SET TimeZone='UTC'"); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { new() { Value = localDateTime.InUtc().ToInstant(), NpgsqlDbType = NpgsqlDbType.TimestampTz} } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected + "+00")); + } + + static NpgsqlParameter[] TimestamptzParameters + { + get + { + var localDateTime = new LocalDateTime(1998, 4, 12, 13, 26, 38); + var instance = localDateTime.InUtc().ToInstant(); + + return new NpgsqlParameter[] + { + new() { Value = instance, NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = instance, DbType = DbType.DateTimeOffset }, + new() { Value = instance.InUtc() }, + new() { Value = instance.WithOffset(Offset.Zero) }, + new() { Value = instance.ToDateTimeOffset() }, + + // In legacy mode we support non-UTC ZonedDateTime and OffsetDateTime + new() { Value = instance.InZone(DateTimeZoneProviders.Tzdb["America/New_York"]), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = instance.WithOffset(Offset.FromHours(1)), NpgsqlDbType = NpgsqlDbType.TimestampTz } + }; + } + } + + [Test, TestCaseSource(nameof(TimestamptzParameters))] + public async Task Timestamptz_resolution(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await conn.ExecuteNonQueryAsync("SET TimeZone='UTC'"); + conn.TypeMapper.Reset(); + conn.TypeMapper.UseNodaTime(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp with time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 13:26:38+00")); + } + + [Test] + public async Task Timestamptz_read_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = + new NpgsqlCommand("SELECT 'infinity'::timestamp with time zone, '-infinity'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetFieldValue(0), Is.EqualTo(Instant.MaxValue)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(DateTime.MaxValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(Instant.MinValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(DateTime.MinValue)); + } + + [Test] + public async Task Timestamptz_write_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = new NpgsqlCommand("SELECT $1::text, $2::text, $3::text, $4::text", conn) + { + Parameters = + { + new() { Value = Instant.MaxValue }, + new() { Value = DateTime.MaxValue }, + new() { Value = Instant.MinValue }, + new() { Value = DateTime.MinValue } + } + }; + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader[0], Is.EqualTo("infinity")); + Assert.That(reader[1], Is.EqualTo("infinity")); + Assert.That(reader[2], Is.EqualTo("-infinity")); + Assert.That(reader[3], Is.EqualTo("-infinity")); + } + + [Test] + public async Task TimeTz() + { + await using var conn = await OpenConnectionAsync(); + var time = new LocalTime(1, 2, 3, 4).PlusNanoseconds(5000); + var offset = Offset.FromHoursAndMinutes(3, 30) + Offset.FromSeconds(5); + var expected = new OffsetTime(time, offset); + var dateTimeOffset = new DateTimeOffset(0001, 01, 02, 03, 43, 20, TimeSpan.FromHours(3)); + var dateTime = dateTimeOffset.DateTime; + + using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3, @p4", conn); + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.TimeTz) { Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter("p3", NpgsqlDbType.TimeTz) { Value = dateTimeOffset }); + cmd.Parameters.Add(new NpgsqlParameter("p4", dateTimeOffset)); + + using var reader = cmd.ExecuteReader(); + reader.Read(); + + for (var i = 0; i < 2; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(OffsetTime))); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); + Assert.That(reader.GetValue(i), Is.EqualTo(expected)); + } + for (var i = 2; i < 4; i++) + { + Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTimeOffset)); + } + } + + #region Support + + protected override async ValueTask OpenConnectionAsync(string? connectionString = null) + { + var conn = new NpgsqlConnection(connectionString ?? ConnectionString); + await conn.OpenAsync(); + conn.TypeMapper.UseNodaTime(); + return conn; + } + + protected override NpgsqlConnection OpenConnection(string? connectionString = null) + => throw new NotSupportedException(); + + [OneTimeSetUp] + public void Setup() + { +#if DEBUG + Internal.NodaTimeUtils.LegacyTimestampBehavior = true; + Util.Statics.LegacyTimestampBehavior = true; +#else + Assert.Ignore( + "Legacy NodaTime tests rely on the Npgsql.EnableLegacyTimestampBehavior AppContext switch and can only be run in DEBUG builds"); +#endif + + } + + [OneTimeTearDown] + public void Teardown() + { +#if DEBUG + Internal.NodaTimeUtils.LegacyTimestampBehavior = false; + Util.Statics.LegacyTimestampBehavior = false; +#endif + } + + #endregion Support + } +} diff --git a/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs new file mode 100644 index 0000000000..a09983455b --- /dev/null +++ b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs @@ -0,0 +1,607 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using NodaTime; +using Npgsql.Tests; +using NpgsqlTypes; +using NUnit.Framework; + +// ReSharper disable AccessToModifiedClosure +// ReSharper disable AccessToDisposedClosure + +namespace Npgsql.NodaTime.Tests +{ + public class NodaTimeTests : TestBase + { + #region Timestamp + + static readonly TestCaseData[] TimestampValues = + { + new TestCaseData(new LocalDateTime(1998, 4, 12, 13, 26, 38, 789), "1998-04-12 13:26:38.789") + .SetName("TimestampPre2000"), + new TestCaseData(new LocalDateTime(2015, 1, 27, 8, 45, 12, 345), "2015-01-27 08:45:12.345") + .SetName("TimestampPost2000"), + new TestCaseData(new LocalDateTime(1999, 12, 31, 23, 59, 59, 999).PlusNanoseconds(456000), "1999-12-31 23:59:59.999456") + .SetName("TimestampMicroseconds"), + }; + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_read(LocalDateTime localDateTime, string s) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}'::timestamp without time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp without time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalDateTime))); + + Assert.That(reader[0], Is.EqualTo(localDateTime)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDateTime)); + Assert.That(reader.GetDateTime(0), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); + + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetDate(0), Throws.TypeOf()); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_write_values(LocalDateTime localDateTime, string expected) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = + { + new() { Value = localDateTime, NpgsqlDbType = NpgsqlDbType.Timestamp } + } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestampParameters + { + get + { + var localDateTime = new LocalDateTime(1998, 4, 12, 13, 26, 38); + + return new NpgsqlParameter[] + { + new() { Value = localDateTime }, + new() { Value = localDateTime, NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = localDateTime, DbType = DbType.DateTime }, + new() { Value = localDateTime, DbType = DbType.DateTime2 }, + new() { Value = localDateTime.ToDateTimeUnspecified() }, + new() { Value = DateTime.SpecifyKind(localDateTime.ToDateTimeUnspecified(), DateTimeKind.Local) } + }; + } + } + + [Test, TestCaseSource(nameof(TimestampParameters))] + public async Task Timestamp_resolution(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + conn.TypeMapper.UseNodaTime(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp without time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 13:26:38")); + } + + static NpgsqlParameter[] TimestampInvalidParameters + => new NpgsqlParameter[] + { + new() { Value = new LocalDateTime().InUtc().ToInstant(), NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = new DateTimeOffset(), NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = DateTime.UtcNow, NpgsqlDbType = NpgsqlDbType.Timestamp } + }; + + [Test, TestCaseSource(nameof(TimestampInvalidParameters))] + public async Task Timestamp_resolution_failure(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + #endregion Timestamp + + #region Timestamp with time zone + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamptz_read(LocalDateTime expectedLocalDateTime, string s) + { + var expectedInstance = expectedLocalDateTime.InUtc().ToInstant(); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}+00'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp with time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(Instant))); + + Assert.That(reader[0], Is.EqualTo(expectedInstance)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.InUtc())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.WithOffset(Offset.Zero))); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.ToDateTimeUtc())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(expectedInstance.ToDateTimeOffset())); + + Assert.That(() => reader.GetFieldValue(0), Throws.TypeOf()); + Assert.That(() => reader.GetDate(0), Throws.TypeOf()); + } + + static readonly TestCaseData[] TimestampTzValues = + { + new TestCaseData(new LocalDateTime(1998, 4, 12, 13, 26, 38, 789), "1998-04-12 15:26:38.789+02") + .SetName("TimestampTzPre2000"), + new TestCaseData(new LocalDateTime(2015, 1, 27, 8, 45, 12, 345), "2015-01-27 09:45:12.345+01") + .SetName("TimestampTzPost2000"), + new TestCaseData(new LocalDateTime(1999, 12, 31, 23, 59, 59, 999).PlusNanoseconds(456000), "2000-01-01 00:59:59.999456+01") + .SetName("TimestampTzMicroseconds"), + }; + + [Test, TestCaseSource(nameof(TimestampTzValues))] + public async Task Timestamptz_write_values(LocalDateTime localDateTime, string expected) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { new() { Value = localDateTime.InUtc().ToInstant(), NpgsqlDbType = NpgsqlDbType.TimestampTz} } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestamptzParameters + { + get + { + var localDateTime = new LocalDateTime(1998, 4, 12, 13, 26, 38); + var instance = localDateTime.InUtc().ToInstant(); + + return new NpgsqlParameter[] + { + new() { Value = instance }, + new() { Value = instance, NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = instance, DbType = DbType.DateTimeOffset }, + new() { Value = instance.InUtc() }, + new() { Value = instance.WithOffset(Offset.Zero) }, + new() { Value = instance.InUtc().ToDateTimeUtc() }, + new() { Value = instance.ToDateTimeOffset() } + }; + } + } + + [Test, TestCaseSource(nameof(TimestamptzParameters))] + public async Task Timestamptz_resolution(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + conn.TypeMapper.UseNodaTime(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp with time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 15:26:38+02")); // We set TimeZone to Europe/Berlin below + } + + static NpgsqlParameter[] TimestamptzInvalidParameters + => new NpgsqlParameter[] + { + new() { Value = new LocalDateTime(), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = DateTime.Now, NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new DateTimeOffset(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), TimeSpan.FromHours(2)), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + + // We only support ZonedDateTime and OffsetDateTime in UTC + new() { Value = new LocalDateTime().InUtc().ToInstant().InZone(DateTimeZoneProviders.Tzdb["America/New_York"]), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new LocalDateTime().WithOffset(Offset.FromHours(1)), NpgsqlDbType = NpgsqlDbType.TimestampTz } + }; + + [Test, TestCaseSource(nameof(TimestamptzInvalidParameters))] + public async Task Timestamptz_resolution_failure(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task Timestamptz_read_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = + new NpgsqlCommand("SELECT 'infinity'::timestamp with time zone, '-infinity'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetFieldValue(0), Is.EqualTo(Instant.MaxValue)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(DateTime.MaxValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(Instant.MinValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(DateTime.MinValue)); + } + + [Test] + public async Task Timestamptz_write_infinity() + { + var connectionString = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }.ConnectionString; + await using var conn = await OpenConnectionAsync(connectionString); + await using var cmd = new NpgsqlCommand("SELECT $1::text, $2::text, $3::text, $4::text", conn) + { + Parameters = + { + new() { Value = Instant.MaxValue }, + new() { Value = DateTime.MaxValue }, + new() { Value = Instant.MinValue }, + new() { Value = DateTime.MinValue } + } + }; + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader[0], Is.EqualTo("infinity")); + Assert.That(reader[1], Is.EqualTo("infinity")); + Assert.That(reader[2], Is.EqualTo("-infinity")); + Assert.That(reader[3], Is.EqualTo("-infinity")); + } + + #endregion Timestamp with time zone + + #region Date + + [Test] + public async Task Date() + { + await using var conn = await OpenConnectionAsync(); + var localDate = new LocalDate(2002, 3, 4); + var dateTime = new DateTime(localDate.Year, localDate.Month, localDate.Day); + + using (var cmd = new NpgsqlCommand("CREATE TEMP TABLE data (d1 DATE, d2 DATE, d3 DATE, d4 DATE, d5 DATE)", conn)) + cmd.ExecuteNonQuery(); + + using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4, @p5)", conn)) + { + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Date) { Value = localDate }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = localDate }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p3", Value = new LocalDate(-5, 3, 3) }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p4", Value = dateTime }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p5", Value = dateTime, NpgsqlDbType = NpgsqlDbType.Date }); + cmd.ExecuteNonQuery(); + } + + using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT, d5::TEXT FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + Assert.That(reader.GetValue(0), Is.EqualTo("2002-03-04")); + Assert.That(reader.GetValue(1), Is.EqualTo("2002-03-04")); + Assert.That(reader.GetValue(2), Is.EqualTo("0006-03-03 BC")); + Assert.That(reader.GetValue(3), Is.EqualTo("2002-03-04")); + Assert.That(reader.GetValue(4), Is.EqualTo("2002-03-04")); + } + + using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalDate))); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDate)); + Assert.That(reader.GetValue(0), Is.EqualTo(localDate)); + Assert.That(() => reader.GetDateTime(0), Is.EqualTo(dateTime)); + Assert.That(() => reader.GetDate(0), Is.EqualTo(new NpgsqlDate(localDate.Year, localDate.Month, localDate.Day))); + Assert.That(reader.GetFieldValue(2), Is.EqualTo(new LocalDate(-5, 3, 3))); + Assert.That(reader.GetFieldValue(3), Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(4), Is.EqualTo(dateTime)); + } + } + +#if NET6_0_OR_GREATER + [Test] + public async Task Date_DateOnly() + { + await using var conn = await OpenConnectionAsync(); + var localDate = new LocalDate(2002, 3, 4); + var dateOnly = new DateOnly(2002, 3, 4); + + using (var cmd = new NpgsqlCommand("CREATE TEMP TABLE data (d1 DATE)", conn)) + cmd.ExecuteNonQuery(); + + using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1)", conn)) + { + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p1", Value = dateOnly }); + cmd.ExecuteNonQuery(); + } + + using (var cmd = new NpgsqlCommand("SELECT d1::TEXT FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + Assert.That(reader.GetValue(0), Is.EqualTo("2002-03-04")); + } + + using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalDate))); + Assert.That(reader.GetValue(0), Is.EqualTo(localDate)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateOnly)); + } + } +#endif + + [Test, Description("Makes sure that when ConvertInfinityDateTime is true, infinity values are properly converted")] + public async Task DateConvertInfinity() + { + var csb = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }; + await using var conn = await OpenConnectionAsync(csb); + conn.ExecuteNonQuery("CREATE TEMP TABLE data (d1 DATE, d2 DATE, d3 DATE, d4 DATE)"); + + using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4)", conn)) + { + cmd.Parameters.AddWithValue("p1", NpgsqlDbType.Date, LocalDate.MaxIsoValue); + cmd.Parameters.AddWithValue("p2", NpgsqlDbType.Date, LocalDate.MinIsoValue); + cmd.Parameters.AddWithValue("p3", NpgsqlDbType.Date, DateTime.MaxValue); + cmd.Parameters.AddWithValue("p4", NpgsqlDbType.Date, DateTime.MinValue); + cmd.ExecuteNonQuery(); + } + + using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + Assert.That(reader.GetValue(0), Is.EqualTo("infinity")); + Assert.That(reader.GetValue(1), Is.EqualTo("-infinity")); + Assert.That(reader.GetValue(2), Is.EqualTo("infinity")); + Assert.That(reader.GetValue(3), Is.EqualTo("-infinity")); + } + + using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(LocalDate.MaxIsoValue)); + Assert.That(reader.GetFieldValue(1), Is.EqualTo(LocalDate.MinIsoValue)); + Assert.That(reader.GetFieldValue(2), Is.EqualTo(DateTime.MaxValue)); + Assert.That(reader.GetFieldValue(3), Is.EqualTo(DateTime.MinValue)); + } + } + + #endregion Date + + #region Time + + [Test] + public async Task Time() + { + await using var conn = await OpenConnectionAsync(); + var expected = new LocalTime(1, 2, 3, 4).PlusNanoseconds(5000); + var timeSpan = new TimeSpan(0, 1, 2, 3, 4).Add(TimeSpan.FromTicks(50)); + + using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3", conn); + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Time) { Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter("p2", DbType.Time) { Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter("p3", DbType.Time) { Value = timeSpan }); + using var reader = cmd.ExecuteReader(); + reader.Read(); + + for (var i = 0; i < cmd.Parameters.Count; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(LocalTime))); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); + Assert.That(reader.GetValue(i), Is.EqualTo(expected)); + Assert.That(() => reader.GetTimeSpan(i), Is.EqualTo(timeSpan)); + } + } + +#if NET6_0_OR_GREATER + [Test] + public async Task Time_TimeOnly() + { + await using var conn = await OpenConnectionAsync(); + var timeOnly = new TimeOnly(1, 2, 3, 500); + var localTime = new LocalTime(1, 2, 3, 500); + + using var cmd = new NpgsqlCommand("SELECT @p1", conn); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p1", Value = timeOnly }); + + using var reader = cmd.ExecuteReader(); + reader.Read(); + + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalTime))); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(timeOnly)); + Assert.That(reader.GetValue(0), Is.EqualTo(localTime)); + } +#endif + + #endregion Time + + #region Time with time zone + + [Test] + public async Task TimeTz() + { + await using var conn = await OpenConnectionAsync(); + var time = new LocalTime(1, 2, 3, 4).PlusNanoseconds(5000); + var offset = Offset.FromHoursAndMinutes(3, 30) + Offset.FromSeconds(5); + var expected = new OffsetTime(time, offset); + var dateTimeOffset = new DateTimeOffset(0001, 01, 02, 03, 43, 20, TimeSpan.FromHours(3)); + + using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3", conn); + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.TimeTz) { Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = expected }); + cmd.Parameters.Add(new NpgsqlParameter("p3", NpgsqlDbType.TimeTz) { Value = dateTimeOffset }); + + using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + + for (var i = 0; i < 2; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(OffsetTime))); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); + Assert.That(reader.GetValue(i), Is.EqualTo(expected)); + } + } + + cmd.CommandText = "SELECT @p"; + cmd.Parameters.Clear(); + cmd.Parameters.Add(new("p", NpgsqlDbType.TimeTz) { Value = DateTime.UtcNow }); + Assert.That(() => cmd.ExecuteReader(), Throws.Exception.TypeOf()); + + cmd.Parameters.Clear(); + cmd.Parameters.Add(new("p", NpgsqlDbType.TimeTz) { Value = TimeSpan.Zero }); + Assert.That(() => cmd.ExecuteReader(), Throws.Exception.TypeOf()); + } + + #endregion Time with time zone + + #region Interval + + [Test] + public async Task IntervalAsPeriod() + { + // PG has microsecond precision, so sub-microsecond values are stripped + var expectedPeriod = new PeriodBuilder + { + Years = 1, + Months = 2, + Weeks = 3, + Days = 4, + Hours = 5, + Minutes = 6, + Seconds = 7, + Milliseconds = 8, + Nanoseconds = 9000 + }.Build().Normalize(); + + await using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expectedPeriod }); + cmd.Parameters.AddWithValue("p2", expectedPeriod); + using var reader = cmd.ExecuteReader(); + reader.Read(); + + for (var i = 0; i < 2; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expectedPeriod)); + Assert.That(reader.GetValue(i), Is.EqualTo(expectedPeriod)); + } + } + + [Test] + public async Task IntervalAsDuration() + { + await using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); + + // PG has microsecond precision, so sub-microsecond values are stripped + var expected = Duration.FromDays(5) + Duration.FromMinutes(4) + Duration.FromSeconds(3) + Duration.FromMilliseconds(2) + + Duration.FromNanoseconds(1500); + + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); + cmd.Parameters.AddWithValue("p2", expected); + using var reader = cmd.ExecuteReader(); + reader.Read(); + for (var i = 0; i < 2; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected - Duration.FromNanoseconds(500))); + } + } + + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3438")] + public async Task Bug3438() + { + await using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); + + var expected = Duration.FromSeconds(2148); + + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); + cmd.Parameters.AddWithValue("p2", expected); + using var reader = cmd.ExecuteReader(); + reader.Read(); + for (var i = 0; i < 2; i++) + { + Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); + } + } + + [Test] + public async Task IntervalAsTimeSpan() + { + var expected = new TimeSpan(1, 2, 3, 4, 5); + await using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); + + cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); + cmd.Parameters.AddWithValue("p2", expected); + using var reader = cmd.ExecuteReader(); + reader.Read(); + + for (var i = 0; i < 2; i++) + { + Assert.That(() => reader.GetTimeSpan(i), Is.EqualTo(expected)); + Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); + } + } + + [Test] + public async Task IntervalAsDurationWithMonthsFails() + { + await using var conn = await OpenConnectionAsync(); + using var cmd = new NpgsqlCommand("SELECT make_interval(months => 2)", conn); + using var reader = cmd.ExecuteReader(); + reader.Read(); + + Assert.That(() => reader.GetFieldValue(0), Throws.Exception.TypeOf().With.Message.EqualTo( + "Cannot read PostgreSQL interval with non-zero months to NodaTime Duration. Try reading as a NodaTime Period instead.")); + } + + #endregion Interval + + #region Support + + protected override async ValueTask OpenConnectionAsync(string? connectionString = null) + { + var conn = await base.OpenConnectionAsync(connectionString); + conn.TypeMapper.UseNodaTime(); + await conn.ExecuteNonQueryAsync("SET TimeZone='Europe/Berlin'"); + return conn; + } + + protected override NpgsqlConnection OpenConnection(string? connectionString = null) + => throw new NotSupportedException(); + + #endregion Support + } +} diff --git a/test/Npgsql.NodaTime.Tests/Npgsql.NodaTime.Tests.csproj b/test/Npgsql.NodaTime.Tests/Npgsql.NodaTime.Tests.csproj new file mode 100644 index 0000000000..6bac9ec314 --- /dev/null +++ b/test/Npgsql.NodaTime.Tests/Npgsql.NodaTime.Tests.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/test/Npgsql.PluginTests/NodaTimeTests.cs b/test/Npgsql.PluginTests/NodaTimeTests.cs deleted file mode 100644 index 580bc50ee3..0000000000 --- a/test/Npgsql.PluginTests/NodaTimeTests.cs +++ /dev/null @@ -1,520 +0,0 @@ -using System; -using System.Data; -using System.Globalization; -using NodaTime; -using Npgsql.Tests; -using NpgsqlTypes; -using NUnit.Framework; - -// ReSharper disable AccessToModifiedClosure -// ReSharper disable AccessToDisposedClosure - -namespace Npgsql.PluginTests -{ - public class NodaTimeTests : TestBase - { - #region Timestamp - - static readonly TestCaseData[] TimestampCases = { - new TestCaseData(new LocalDateTime(1998, 4, 12, 13, 26, 38, 789)).SetName(nameof(Timestamp) + "Pre2000"), - new TestCaseData(new LocalDateTime(2015, 1, 27, 8, 45, 12, 345)).SetName(nameof(Timestamp) + "Post2000"), - new TestCaseData(new LocalDateTime(1999, 12, 31, 23, 59, 59, 999).PlusNanoseconds(456000)).SetName(nameof(Timestamp) + "Microseconds"), - }; - - [Test, TestCaseSource(nameof(TimestampCases))] - public void Timestamp(LocalDateTime localDateTime) - { - using var conn = OpenConnection(); - var instant = localDateTime.InUtc().ToInstant(); - var minTimestampPostgres = Instant.FromUtc(-4713, 12, 31, 00, 00, 00); - var maxTimestampPostgres = Instant.MaxValue; - var dateTime = new DateTime(2020, 03, 04, 12, 20, 44, 0, DateTimeKind.Utc); - - conn.ExecuteNonQuery("CREATE TEMP TABLE data (d1 TIMESTAMP, d2 TIMESTAMP, d3 TIMESTAMP, d4 TIMESTAMP, d5 TIMESTAMP, d6 TIMESTAMP, d7 TIMESTAMP, d8 TIMESTAMP)"); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8)", conn)) - { - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Timestamp) { Value = instant }); - cmd.Parameters.Add(new NpgsqlParameter("p2", DbType.DateTime) { Value = instant }); - cmd.Parameters.Add(new NpgsqlParameter("p3", DbType.DateTime2) { Value = instant }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p4", Value = instant }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p5", Value = localDateTime }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p6", Value = minTimestampPostgres }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p7", Value = maxTimestampPostgres }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p8", Value = dateTime }); - cmd.ExecuteNonQuery(); - } - - // Make sure the values inserted are the good ones, textually - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT, d5::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - for (var i = 0; i < reader.FieldCount; i++) - Assert.That(reader.GetValue(i), Is.EqualTo(instant.ToString("yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'FFFFFF", CultureInfo.InvariantCulture))); - } - - using (var cmd = new NpgsqlCommand("SELECT d6::TEXT, d7::TEXT, d8::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetValue(0), Is.EqualTo("4714-12-31 00:00:00 BC")); - Assert.That(reader.GetValue(1), Is.EqualTo(maxTimestampPostgres.ToString("yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'FFFFFF", CultureInfo.InvariantCulture))); - Assert.That(reader.GetValue(2), Is.EqualTo("2020-03-04 12:20:44")); - } - - using (var cmd = new NpgsqlCommand("SELECT d1, d2, d3, d4, d5 FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - - for (var i = 0; i < reader.FieldCount; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Instant))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(instant)); - Assert.That(reader.GetValue(i), Is.EqualTo(instant)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(localDateTime)); - Assert.That(() => reader.GetFieldValue(i), Throws.TypeOf()); - Assert.That(() => reader.GetDateTime(i), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); - Assert.That(() => reader.GetFieldValue(i), Is.EqualTo(localDateTime.ToDateTimeUnspecified())); - Assert.That(() => reader.GetDate(i), Throws.TypeOf()); - } - } - } - - [Test, Description("Makes sure that when ConvertInfinityDateTime is true, infinity values are properly converted")] - public void TimestampConvertInfinity() - { - var csb = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }; - using var conn = OpenConnection(csb); - conn.ExecuteNonQuery("CREATE TEMP TABLE data (d1 TIMESTAMP, d2 TIMESTAMP, d3 TIMESTAMP, d4 TIMESTAMP)"); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4)", conn)) - { - cmd.Parameters.AddWithValue("p1", NpgsqlDbType.Timestamp, Instant.MaxValue); - cmd.Parameters.AddWithValue("p2", NpgsqlDbType.Timestamp, Instant.MinValue); - cmd.Parameters.AddWithValue("p3", NpgsqlDbType.Timestamp, DateTime.MaxValue); - cmd.Parameters.AddWithValue("p4", NpgsqlDbType.Timestamp, DateTime.MinValue); - cmd.ExecuteNonQuery(); - } - - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetValue(0), Is.EqualTo("infinity")); - Assert.That(reader.GetValue(1), Is.EqualTo("-infinity")); - Assert.That(reader.GetValue(2), Is.EqualTo("infinity")); - Assert.That(reader.GetValue(3), Is.EqualTo("-infinity")); - } - - using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetFieldValue(0), Is.EqualTo(Instant.MaxValue)); - Assert.That(reader.GetFieldValue(1), Is.EqualTo(Instant.MinValue)); - Assert.That(reader.GetFieldValue(2), Is.EqualTo(DateTime.MaxValue)); - Assert.That(reader.GetFieldValue(3), Is.EqualTo(DateTime.MinValue)); - } - } - - #endregion Timestamp - - #region Timestamp with time zone - - [Test] - public void TimestampTz() - { - using var conn = OpenConnection(); - var timezone = "America/New_York"; - conn.ExecuteNonQuery($"SET TIMEZONE TO '{timezone}'"); - Assert.That(conn.Timezone, Is.EqualTo(timezone)); - // Nodatime provider should return timestamptz's as ZonedDateTime in the session timezone - - var instant = Instant.FromUtc(2015, 6, 27, 8, 45, 12) + Duration.FromMilliseconds(345); - var utcZonedDateTime = instant.InUtc(); - var localZonedDateTime = utcZonedDateTime.WithZone(DateTimeZoneProviders.Tzdb[timezone]); - var offsetDateTime = localZonedDateTime.ToOffsetDateTime(); - var dateTimeOffset = offsetDateTime.ToDateTimeOffset(); - var dateTime = dateTimeOffset.DateTime; - var localDateTime = dateTimeOffset.LocalDateTime; - - conn.ExecuteNonQuery("CREATE TEMP TABLE data (d1 TIMESTAMPTZ, d2 TIMESTAMPTZ, d3 TIMESTAMPTZ, d4 TIMESTAMPTZ, d5 TIMESTAMPTZ, d6 TIMESTAMPTZ)"); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4, @p5, @p6)", conn)) - { - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.TimestampTz) { Value = instant }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = utcZonedDateTime }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p3", Value = localZonedDateTime }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p4", Value = offsetDateTime }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p5", Value = dateTimeOffset }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p6", Value = dateTime }); - cmd.ExecuteNonQuery(); - } - - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT, d5::TEXT, d6::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - // When converting timestamptz as a string as we're doing here, PostgreSQL automatically converts - // it to the session timezone - for (var i = 0; i < reader.FieldCount; i++) - Assert.That(reader.GetValue(i), Is.EqualTo( - localZonedDateTime.ToString("uuuu'-'MM'-'dd' 'HH':'mm':'ss'.'fff", CultureInfo.InvariantCulture) + "-04") - ); - } - - using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - - for (var i = 0; i < reader.FieldCount; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Instant))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(instant)); - Assert.That(reader.GetValue(i), Is.EqualTo(instant)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(localZonedDateTime)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(offsetDateTime)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTimeOffset)); - Assert.That(() => reader.GetFieldValue(i), Throws.TypeOf()); - Assert.That(() => reader.GetDateTime(i), Is.EqualTo(localDateTime)); - Assert.That(() => reader.GetDate(i), Throws.TypeOf()); - } - } - } - - #endregion Timestamp with time zone - - #region Date - - [Test] - public void Date() - { - using var conn = OpenConnection(); - var localDate = new LocalDate(2002, 3, 4); - var dateTime = new DateTime(localDate.Year, localDate.Month, localDate.Day); - - using (var cmd = new NpgsqlCommand("CREATE TEMP TABLE data (d1 DATE, d2 DATE, d3 DATE, d4 DATE, d5 DATE)", conn)) - cmd.ExecuteNonQuery(); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4, @p5)", conn)) - { - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Date) { Value = localDate }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = localDate }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p3", Value = new LocalDate(-5, 3, 3) }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p4", Value = dateTime }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p5", Value = dateTime, NpgsqlDbType = NpgsqlDbType.Date }); - cmd.ExecuteNonQuery(); - } - - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT, d5::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetValue(0), Is.EqualTo("2002-03-04")); - Assert.That(reader.GetValue(1), Is.EqualTo("2002-03-04")); - Assert.That(reader.GetValue(2), Is.EqualTo("0006-03-03 BC")); - Assert.That(reader.GetValue(3), Is.EqualTo("2002-03-04")); - Assert.That(reader.GetValue(4), Is.EqualTo("2002-03-04")); - } - - using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - - Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalDate))); - Assert.That(reader.GetFieldValue(0), Is.EqualTo(localDate)); - Assert.That(reader.GetValue(0), Is.EqualTo(localDate)); - Assert.That(() => reader.GetDateTime(0), Is.EqualTo(dateTime)); - Assert.That(() => reader.GetDate(0), Is.EqualTo(new NpgsqlDate(localDate.Year, localDate.Month, localDate.Day))); - Assert.That(reader.GetFieldValue(2), Is.EqualTo(new LocalDate(-5, 3, 3))); - Assert.That(reader.GetFieldValue(3), Is.EqualTo(dateTime)); - Assert.That(reader.GetDateTime(4), Is.EqualTo(dateTime)); - } - } - -#if NET6_0_OR_GREATER - [Test] - public void Date_DateOnly() - { - using var conn = OpenConnection(); - var localDate = new LocalDate(2002, 3, 4); - var dateOnly = new DateOnly(2002, 3, 4); - - using (var cmd = new NpgsqlCommand("CREATE TEMP TABLE data (d1 DATE)", conn)) - cmd.ExecuteNonQuery(); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1)", conn)) - { - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p1", Value = dateOnly }); - cmd.ExecuteNonQuery(); - } - - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetValue(0), Is.EqualTo("2002-03-04")); - } - - using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - - Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalDate))); - Assert.That(reader.GetValue(0), Is.EqualTo(localDate)); - Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateOnly)); - } - } -#endif - - [Test, Description("Makes sure that when ConvertInfinityDateTime is true, infinity values are properly converted")] - public void DateConvertInfinity() - { - var csb = new NpgsqlConnectionStringBuilder(ConnectionString) { ConvertInfinityDateTime = true }; - using var conn = OpenConnection(csb); - conn.ExecuteNonQuery("CREATE TEMP TABLE data (d1 DATE, d2 DATE, d3 DATE, d4 DATE)"); - - using (var cmd = new NpgsqlCommand("INSERT INTO data VALUES (@p1, @p2, @p3, @p4)", conn)) - { - cmd.Parameters.AddWithValue("p1", NpgsqlDbType.Date, LocalDate.MaxIsoValue); - cmd.Parameters.AddWithValue("p2", NpgsqlDbType.Date, LocalDate.MinIsoValue); - cmd.Parameters.AddWithValue("p3", NpgsqlDbType.Date, DateTime.MaxValue); - cmd.Parameters.AddWithValue("p4", NpgsqlDbType.Date, DateTime.MinValue); - cmd.ExecuteNonQuery(); - } - - using (var cmd = new NpgsqlCommand("SELECT d1::TEXT, d2::TEXT, d3::TEXT, d4::TEXT FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetValue(0), Is.EqualTo("infinity")); - Assert.That(reader.GetValue(1), Is.EqualTo("-infinity")); - Assert.That(reader.GetValue(2), Is.EqualTo("infinity")); - Assert.That(reader.GetValue(3), Is.EqualTo("-infinity")); - } - - using (var cmd = new NpgsqlCommand("SELECT * FROM data", conn)) - using (var reader = cmd.ExecuteReader()) - { - reader.Read(); - Assert.That(reader.GetFieldValue(0), Is.EqualTo(LocalDate.MaxIsoValue)); - Assert.That(reader.GetFieldValue(1), Is.EqualTo(LocalDate.MinIsoValue)); - Assert.That(reader.GetFieldValue(2), Is.EqualTo(DateTime.MaxValue)); - Assert.That(reader.GetFieldValue(3), Is.EqualTo(DateTime.MinValue)); - } - } - - #endregion Date - - #region Time - - [Test] - public void Time() - { - using var conn = OpenConnection(); - var expected = new LocalTime(1, 2, 3, 4).PlusNanoseconds(5000); - var timeSpan = new TimeSpan(0, 1, 2, 3, 4).Add(TimeSpan.FromTicks(50)); - - using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3", conn); - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Time) { Value = expected }); - cmd.Parameters.Add(new NpgsqlParameter("p2", DbType.Time) { Value = expected }); - cmd.Parameters.Add(new NpgsqlParameter("p3", DbType.Time) { Value = timeSpan }); - using var reader = cmd.ExecuteReader(); - reader.Read(); - - for (var i = 0; i < cmd.Parameters.Count; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(LocalTime))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); - Assert.That(reader.GetValue(i), Is.EqualTo(expected)); - Assert.That(() => reader.GetTimeSpan(i), Is.EqualTo(timeSpan)); - } - } - -#if NET6_0_OR_GREATER - [Test] - public void Time_TimeOnly() - { - using var conn = OpenConnection(); - var timeOnly = new TimeOnly(1, 2, 3, 500); - var localTime = new LocalTime(1, 2, 3, 500); - - using var cmd = new NpgsqlCommand("SELECT @p1", conn); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p1", Value = timeOnly }); - - using var reader = cmd.ExecuteReader(); - reader.Read(); - - Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(LocalTime))); - Assert.That(reader.GetFieldValue(0), Is.EqualTo(timeOnly)); - Assert.That(reader.GetValue(0), Is.EqualTo(localTime)); - } -#endif - - #endregion Time - - #region Time with time zone - - [Test] - public void TimeTz() - { - using var conn = OpenConnection(); - var time = new LocalTime(1, 2, 3, 4).PlusNanoseconds(5000); - var offset = Offset.FromHoursAndMinutes(3, 30) + Offset.FromSeconds(5); - var expected = new OffsetTime(time, offset); - var dateTimeOffset = new DateTimeOffset(0001, 01, 02, 03, 43, 20, TimeSpan.FromHours(3)); - var dateTime = dateTimeOffset.DateTime; - - using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3, @p4, @p5, @p6", conn); - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.TimeTz) { Value = expected }); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2", Value = expected }); - cmd.Parameters.Add(new NpgsqlParameter("p3", NpgsqlDbType.TimeTz) { Value = dateTimeOffset }); - cmd.Parameters.Add(new NpgsqlParameter("p4", dateTimeOffset)); - cmd.Parameters.Add(new NpgsqlParameter("p5", NpgsqlDbType.TimeTz) { Value = dateTime }); - cmd.Parameters.Add(new NpgsqlParameter("p6", dateTime)); - - using var reader = cmd.ExecuteReader(); - reader.Read(); - - for (var i = 0; i < 2; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(OffsetTime))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); - Assert.That(reader.GetValue(i), Is.EqualTo(expected)); - } - for (var i = 2; i < 4; i++) - { - Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTimeOffset)); - } - for (var i = 4; i < 6; i++) - { - Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTime)); - } - } - - #endregion Time with time zone - - #region Interval - - [Test] - public void IntervalAsPeriod() - { - // PG has microsecond precision, so sub-microsecond values are stripped - var expectedPeriod = new PeriodBuilder - { - Years = 1, - Months = 2, - Weeks = 3, - Days = 4, - Hours = 5, - Minutes = 6, - Seconds = 7, - Milliseconds = 8, - Nanoseconds = 9000 - }.Build().Normalize(); - - using var conn = OpenConnection(); - using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expectedPeriod }); - cmd.Parameters.AddWithValue("p2", expectedPeriod); - using var reader = cmd.ExecuteReader(); - reader.Read(); - - for (var i = 0; i < 2; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(expectedPeriod)); - Assert.That(reader.GetValue(i), Is.EqualTo(expectedPeriod)); - } - } - - [Test] - public void IntervalAsDuration() - { - using var conn = OpenConnection(); - using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); - - // PG has microsecond precision, so sub-microsecond values are stripped - var expected = Duration.FromDays(5) + Duration.FromMinutes(4) + Duration.FromSeconds(3) + Duration.FromMilliseconds(2) + - Duration.FromNanoseconds(1500); - - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); - cmd.Parameters.AddWithValue("p2", expected); - using var reader = cmd.ExecuteReader(); - reader.Read(); - for (var i = 0; i < 2; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected - Duration.FromNanoseconds(500))); - } - } - - [Test, IssueLink("https://github.com/npgsql/npgsql/issues/3438")] - public void Bug3438() - { - using var conn = OpenConnection(); - using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); - - var expected = Duration.FromSeconds(2148); - - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); - cmd.Parameters.AddWithValue("p2", expected); - using var reader = cmd.ExecuteReader(); - reader.Read(); - for (var i = 0; i < 2; i++) - { - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(Period))); - } - } - - [Test] - public void IntervalAsTimeSpan() - { - var expected = new TimeSpan(1, 2, 3, 4, 5); - using var conn = OpenConnection(); - using var cmd = new NpgsqlCommand("SELECT @p1, @p2", conn); - - cmd.Parameters.Add(new NpgsqlParameter("p1", NpgsqlDbType.Interval) { Value = expected }); - cmd.Parameters.AddWithValue("p2", expected); - using var reader = cmd.ExecuteReader(); - reader.Read(); - - for (var i = 0; i < 2; i++) - { - Assert.That(() => reader.GetTimeSpan(i), Is.EqualTo(expected)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(expected)); - } - } - - [Test] - public void IntervalAsDurationWithMonthsFails() - { - using var conn = OpenConnection(); - using var cmd = new NpgsqlCommand("SELECT make_interval(months => 2)", conn); - using var reader = cmd.ExecuteReader(); - reader.Read(); - - Assert.That(() => reader.GetFieldValue(0), Throws.Exception.TypeOf().With.Message.EqualTo( - "Cannot read PostgreSQL interval with non-zero months to NodaTime Duration. Try reading as a NodaTime Period instead.")); - } - - #endregion Interval - - #region Support - - protected override NpgsqlConnection OpenConnection(string? connectionString = null) - { - var conn = new NpgsqlConnection(connectionString ?? ConnectionString); - conn.Open(); - conn.TypeMapper.UseNodaTime(); - return conn; - } - - #endregion Support - } -} diff --git a/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj b/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj index a6d33fa15a..88d7bdfef3 100644 --- a/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj +++ b/test/Npgsql.PluginTests/Npgsql.PluginTests.csproj @@ -3,7 +3,6 @@ false - @@ -12,7 +11,6 @@ - diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index 69e8a05b5d..c6ad6ed745 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -2091,25 +2091,26 @@ public ReaderTests(MultiplexingMode multiplexingMode, CommandBehavior behavior) #region Mock Type Handlers - class ExplodingTypeHandlerResolverFactory : ITypeHandlerResolverFactory + class ExplodingTypeHandlerResolverFactory : TypeHandlerResolverFactory { readonly bool _safe; public ExplodingTypeHandlerResolverFactory(bool safe) => _safe = safe; - public ITypeHandlerResolver Create(NpgsqlConnector connector) => new ExplodingTypeHandlerResolver(_safe); + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new ExplodingTypeHandlerResolver(_safe); - public TypeMappingInfo GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); - public string? GetDataTypeNameByClrType(Type type) => throw new NotSupportedException(); + public override TypeMappingInfo GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); + public override string? GetDataTypeNameByClrType(Type clrType) => throw new NotSupportedException(); + public override string? GetDataTypeNameByValueDependentValue(object value) => throw new NotSupportedException(); - class ExplodingTypeHandlerResolver : ITypeHandlerResolver + class ExplodingTypeHandlerResolver : TypeHandlerResolver { readonly bool _safe; public ExplodingTypeHandlerResolver(bool safe) => _safe = safe; - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName == "integer" ? new ExplodingTypeHandler(null!, _safe) : null; - public NpgsqlTypeHandler? ResolveByClrType(Type type) => null; - public TypeMappingInfo GetMappingByDataTypeName(string dataTypeName) => throw new NotImplementedException(); + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => null; + public override TypeMappingInfo GetMappingByDataTypeName(string dataTypeName) => throw new NotImplementedException(); } } diff --git a/test/Npgsql.Tests/TestUtil.cs b/test/Npgsql.Tests/TestUtil.cs index 244612ddcf..f25fad75c8 100644 --- a/test/Npgsql.Tests/TestUtil.cs +++ b/test/Npgsql.Tests/TestUtil.cs @@ -326,60 +326,38 @@ internal static string EncodeByteaHex(ICollection buf) internal static IDisposable SetEnvironmentVariable(string name, string? value) { - var resetter = new EnvironmentVariableResetter(name, Environment.GetEnvironmentVariable(name)); + var oldValue = Environment.GetEnvironmentVariable(name); Environment.SetEnvironmentVariable(name, value); - return resetter; + return new DeferredExecutionDisposable(() => Environment.SetEnvironmentVariable(name, oldValue)); } internal static IDisposable SetCurrentCulture(CultureInfo culture) - => new CultureSetter(culture); + { + var oldCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = culture; + + return new DeferredExecutionDisposable(() => CultureInfo.CurrentCulture = oldCulture); + } internal static IDisposable DisableSqlRewriting() { #if DEBUG NpgsqlCommand.EnableSqlRewriting = false; - return new SqlRewritingEnabler(); + return new DeferredExecutionDisposable(() => NpgsqlCommand.EnableSqlRewriting = true); #else Assert.Ignore("Cannot disable SQL rewriting in RELEASE builds"); throw new NotSupportedException("Cannot disable SQL rewriting in RELEASE builds"); #endif } - class EnvironmentVariableResetter : IDisposable - { - readonly string _name; - readonly string? _value; - - internal EnvironmentVariableResetter(string name, string? value) - { - _name = name; - _value = value; - } - - public void Dispose() => Environment.SetEnvironmentVariable(_name, _value); - } - - class CultureSetter : IDisposable + class DeferredExecutionDisposable : IDisposable { - readonly CultureInfo _oldCulture; + readonly Action _action; - internal CultureSetter(CultureInfo newCulture) - { - _oldCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = newCulture; - } - - public void Dispose() => CultureInfo.CurrentCulture = _oldCulture; - } + internal DeferredExecutionDisposable(Action action) => _action = action; - class SqlRewritingEnabler : IDisposable - { public void Dispose() - { -#if DEBUG - NpgsqlCommand.EnableSqlRewriting = true; -#endif - } + => _action(); } } diff --git a/test/Npgsql.Tests/TypeMapperTests.cs b/test/Npgsql.Tests/TypeMapperTests.cs index e44f289c0a..e74f8df9c8 100644 --- a/test/Npgsql.Tests/TypeMapperTests.cs +++ b/test/Npgsql.Tests/TypeMapperTests.cs @@ -125,31 +125,31 @@ public async Task StringToCitext() #region Support - class MyInt32TypeHandlerResolverFactory : ITypeHandlerResolverFactory + class MyInt32TypeHandlerResolverFactory : TypeHandlerResolverFactory { internal int Reads, Writes; - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new MyInt32TypeHandlerResolver(connector, this); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); - public TypeMappingInfo? GetMappingByClrType(Type clrType) => throw new NotSupportedException(); - public string? GetDataTypeNameByClrType(Type type) => throw new NotSupportedException(); + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); + public override string? GetDataTypeNameByClrType(Type clrType) => throw new NotSupportedException(); + public override string? GetDataTypeNameByValueDependentValue(object value) => throw new NotSupportedException(); } - class MyInt32TypeHandlerResolver : ITypeHandlerResolver + class MyInt32TypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlTypeHandler _handler; public MyInt32TypeHandlerResolver(NpgsqlConnector connector, MyInt32TypeHandlerResolverFactory factory) => _handler = new MyInt32Handler(connector.DatabaseInfo.GetPostgresTypeByName("integer"), factory); - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => type == typeof(int) ? _handler : null; - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => typeName == "integer" ? _handler : null; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); } @@ -174,16 +174,16 @@ public override void Write(int value, NpgsqlWriteBuffer buf, NpgsqlParameter? pa } } - class CitextToStringTypeHandlerResolverFactory : ITypeHandlerResolverFactory + class CitextToStringTypeHandlerResolverFactory : TypeHandlerResolverFactory { - public ITypeHandlerResolver Create(NpgsqlConnector connector) + public override TypeHandlerResolver Create(NpgsqlConnector connector) => new CitextToStringTypeHandlerResolver(connector); - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); - public TypeMappingInfo? GetMappingByClrType(Type clrType) => throw new NotSupportedException(); - public string? GetDataTypeNameByClrType(Type type) => throw new NotSupportedException(); + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); + public override string? GetDataTypeNameByClrType(Type clrType) => throw new NotSupportedException(); + public override string? GetDataTypeNameByValueDependentValue(object value) => throw new NotSupportedException(); - class CitextToStringTypeHandlerResolver : ITypeHandlerResolver + class CitextToStringTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlConnector _connector; readonly PostgresType _pgCitextType; @@ -194,11 +194,11 @@ public CitextToStringTypeHandlerResolver(NpgsqlConnector connector) _pgCitextType = connector.DatabaseInfo.GetPostgresTypeByName("citext"); } - public NpgsqlTypeHandler? ResolveByClrType(Type type) + public override NpgsqlTypeHandler? ResolveByClrType(Type type) => type == typeof(string) ? new TextHandler(_pgCitextType, _connector.TextEncoding) : null; - public NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => null; + public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) => null; - public TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); + public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName) => throw new NotSupportedException(); } } diff --git a/test/Npgsql.Tests/Types/DateTimeTests.cs b/test/Npgsql.Tests/Types/DateTimeTests.cs index 7d3a03b7d0..73cb8d93cb 100644 --- a/test/Npgsql.Tests/Types/DateTimeTests.cs +++ b/test/Npgsql.Tests/Types/DateTimeTests.cs @@ -4,15 +4,10 @@ using System.Threading.Tasks; using NpgsqlTypes; using NUnit.Framework; +using static Npgsql.Tests.TestUtil; namespace Npgsql.Tests.Types { - /// - /// Tests on PostgreSQL date/time types - /// - /// - /// https://www.postgresql.org/docs/current/static/datatype-datetime.html - /// public class DateTimeTests : MultiplexingTestBase { #region Date @@ -194,17 +189,9 @@ public async Task TimeTz() // Note that the date component of the below is ignored var dto = new DateTimeOffset(5, 5, 5, 13, 3, 45, 510, tzOffset); - var dtUtc = new DateTime(dto.Year, dto.Month, dto.Day, dto.Hour, dto.Minute, dto.Second, dto.Millisecond, DateTimeKind.Utc) - tzOffset; - var dtLocal = new DateTime(dto.Year, dto.Month, dto.Day, dto.Hour, dto.Minute, dto.Second, dto.Millisecond, DateTimeKind.Local); - var dtUnspecified = new DateTime(dto.Year, dto.Month, dto.Day, dto.Hour, dto.Minute, dto.Second, dto.Millisecond, DateTimeKind.Unspecified); - var ts = dto.TimeOfDay; - - using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3, @p4, @p5", conn); - cmd.Parameters.AddWithValue("p1", NpgsqlDbType.TimeTz, dto); - cmd.Parameters.AddWithValue("p2", NpgsqlDbType.TimeTz, dtUtc); - cmd.Parameters.AddWithValue("p3", NpgsqlDbType.TimeTz, dtLocal); - cmd.Parameters.AddWithValue("p4", NpgsqlDbType.TimeTz, dtUnspecified); - cmd.Parameters.AddWithValue("p5", NpgsqlDbType.TimeTz, ts); + + using var cmd = new NpgsqlCommand("SELECT @p", conn); + cmd.Parameters.AddWithValue("p", NpgsqlDbType.TimeTz, dto); Assert.That(cmd.Parameters.All(p => p.DbType == DbType.Object)); using var reader = await cmd.ExecuteReaderAsync(); @@ -213,12 +200,8 @@ public async Task TimeTz() for (var i = 0; i < cmd.Parameters.Count; i++) { Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(DateTimeOffset))); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(new DateTimeOffset(1, 1, 2, dto.Hour, dto.Minute, dto.Second, dto.Millisecond, dto.Offset))); Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(DateTimeOffset))); - Assert.That(reader.GetFieldValue(i).Kind, Is.EqualTo(DateTimeKind.Local)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(reader.GetFieldValue(i).LocalDateTime)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(reader.GetFieldValue(i).LocalDateTime.TimeOfDay)); } } @@ -236,67 +219,129 @@ public async Task TimeWithTimeZoneBeforeUtcZero() #region Timestamp - static readonly TestCaseData[] TimeStampCases = { - new TestCaseData(new DateTime(1998, 4, 12, 13, 26, 38)).SetName(nameof(Timestamp) + "Pre2000"), - new TestCaseData(new DateTime(2015, 1, 27, 8, 45, 12, 345)).SetName(nameof(Timestamp) + "Post2000"), - new TestCaseData(new DateTime(2013, 7, 25)).SetName(nameof(Timestamp) + "DateOnly"), + static readonly TestCaseData[] TimestampValues = + { + new TestCaseData(new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), "1998-04-12 13:26:38") + .SetName("TimestampPre2000"), + new TestCaseData(new DateTime(2015, 1, 27, 8, 45, 12, 345, DateTimeKind.Utc), "2015-01-27 08:45:12.345") + .SetName("TimestampPost2000"), + new TestCaseData(new DateTime(2013, 7, 25, 0, 0, 0, DateTimeKind.Utc), "2013-07-25 00:00:00") + .SetName("TimestampDateOnly") }; - [Test, TestCaseSource(nameof(TimeStampCases))] - public async Task Timestamp(DateTime dateTime) + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_read(DateTime dateTime, string s) { - using var conn = await OpenConnectionAsync(); + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}'::timestamp without time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp without time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(DateTime))); + + Assert.That(reader[0], Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0), Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0).Kind, Is.EqualTo(DateTimeKind.Unspecified)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateTime)); + + // Provider-specific type (NpgsqlTimeStamp) var npgsqlDateTime = new NpgsqlDateTime(dateTime.Ticks); + Assert.That(reader.GetProviderSpecificFieldType(0), Is.EqualTo(typeof(NpgsqlDateTime))); + Assert.That(reader.GetTimeStamp(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetProviderSpecificValue(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(npgsqlDateTime)); - using var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3, @p4, @p5, @p6", conn); - var p1 = new NpgsqlParameter("p1", NpgsqlDbType.Timestamp); - var p2 = new NpgsqlParameter("p2", DbType.DateTime); - var p3 = new NpgsqlParameter("p3", DbType.DateTime2); - var p4 = new NpgsqlParameter { ParameterName = "p4", Value = npgsqlDateTime }; - var p5 = new NpgsqlParameter { ParameterName = "p5", Value = dateTime }; - var p6 = new NpgsqlParameter { ParameterName = "p6", TypedValue = dateTime }; - Assert.That(p4.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); - Assert.That(p4.DbType, Is.EqualTo(DbType.DateTime)); - Assert.That(p5.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); - Assert.That(p5.DbType, Is.EqualTo(DbType.DateTime)); - cmd.Parameters.Add(p1); - cmd.Parameters.Add(p2); - cmd.Parameters.Add(p3); - cmd.Parameters.Add(p4); - cmd.Parameters.Add(p5); - cmd.Parameters.Add(p6); - p1.Value = p2.Value = p3.Value = npgsqlDateTime; - using var reader = await cmd.ExecuteReaderAsync(); - reader.Read(); + // DateTimeOffset + Assert.That(() => reader.GetFieldValue(0), Throws.Exception.TypeOf()); + } - for (var i = 0; i < cmd.Parameters.Count; i++) + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_write_values(DateTime dateTime, string expected) + { + Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) { - // Regular type (DateTime) - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(DateTime))); - Assert.That(reader.GetDateTime(i), Is.EqualTo(dateTime)); - Assert.That(reader.GetDateTime(i).Kind, Is.EqualTo(DateTimeKind.Unspecified)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTime)); - Assert.That(reader[i], Is.EqualTo(dateTime)); - Assert.That(reader.GetValue(i), Is.EqualTo(dateTime)); + Parameters = + { + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.Timestamp } + } + }; - // Provider-specific type (NpgsqlTimeStamp) - Assert.That(reader.GetTimeStamp(i), Is.EqualTo(npgsqlDateTime)); - Assert.That(reader.GetProviderSpecificFieldType(i), Is.EqualTo(typeof(NpgsqlDateTime))); - Assert.That(reader.GetProviderSpecificValue(i), Is.EqualTo(npgsqlDateTime)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(npgsqlDateTime)); + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } - // DateTimeOffset - Assert.That(() => reader.GetFieldValue(i), Throws.Exception.TypeOf()); + static NpgsqlParameter[] TimestampParameters + { + get + { + var dateTime = new DateTime(1998, 4, 12, 13, 26, 38); + + return new NpgsqlParameter[] + { + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified) }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Local) }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Local), NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Local), DbType = DbType.DateTime }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Local), DbType = DbType.DateTime2 }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Unspecified) }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Local) }, + }; } } - static readonly TestCaseData[] TimeStampSpecialCases = { + [Test, TestCaseSource(nameof(TimestampParameters))] + public async Task Timestamp_resolution(NpgsqlParameter parameter) + { + if (IsMultiplexing) + return; // conn.TypeMapper.Reset + + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); + Assert.That(parameter.DbType, Is.EqualTo(DbType.DateTime).Or.EqualTo(DbType.DateTime2)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp without time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 13:26:38")); + } + + static NpgsqlParameter[] TimestampInvalidParameters + => new NpgsqlParameter[] + { + new() { Value = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc), NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = new NpgsqlDateTime(0, DateTimeKind.Utc), NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = new DateTimeOffset(DateTime.UtcNow, TimeSpan.Zero), NpgsqlDbType = NpgsqlDbType.Timestamp } + }; + + [Test, TestCaseSource(nameof(TimestampInvalidParameters))] + public async Task Timestamp_resolution_failure(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + static readonly TestCaseData[] TimestampSpecialCases = { new TestCaseData(NpgsqlDateTime.Infinity).SetName(nameof(TimeStampSpecial) + "Infinity"), new TestCaseData(NpgsqlDateTime.NegativeInfinity).SetName(nameof(TimeStampSpecial) + "NegativeInfinity"), new TestCaseData(new NpgsqlDateTime(-5, 3, 3, 1, 0, 0)).SetName(nameof(TimeStampSpecial) + "BC"), }; - [Test, TestCaseSource(nameof(TimeStampSpecialCases))] + [Test, TestCaseSource(nameof(TimestampSpecialCases))] public async Task TimeStampSpecial(NpgsqlDateTime value) { using var conn = await OpenConnectionAsync(); @@ -327,90 +372,206 @@ public async Task TimeStampConvertInfinity() Assert.That(reader.GetDateTime(1), Is.EqualTo(DateTime.MinValue)); } - #endregion + [Test] + public async Task Timestamp_array_resolution() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { new() { Value = new[] { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Local) } } } + }; - #region Timestamp with timezone + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("timestamp without time zone[]")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Array | NpgsqlDbType.Timestamp)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp without time zone[]")); + Assert.That(reader[1], Is.EqualTo(@"{""1998-04-12 13:26:38""}")); + } [Test] - public async Task TimestampTz() + public async Task Timestamp_range_resolution() { - using var conn = await OpenConnectionAsync(); - var tzOffset = TimeZoneInfo.Local.BaseUtcOffset; - if (tzOffset == TimeSpan.Zero) - Assert.Ignore("Test cannot run when machine timezone is UTC"); - - var dateTimeUtc = new DateTime(2015, 6, 27, 8, 45, 12, 345, DateTimeKind.Utc); - var dateTimeLocal = dateTimeUtc.ToLocalTime(); - var dateTimeUnspecified = new DateTime(dateTimeUtc.Ticks, DateTimeKind.Unspecified); + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = + { + new() + { + Value = new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Local), + new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Local)) + } + } + }; - var nDateTimeUtc = new NpgsqlDateTime(dateTimeUtc); - var nDateTimeLocal = nDateTimeUtc.ToLocalTime(); - var nDateTimeUnspecified = new NpgsqlDateTime(nDateTimeUtc.Ticks, DateTimeKind.Unspecified); + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("tsrange")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Range | NpgsqlDbType.Timestamp)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); - //var dateTimeOffset = new DateTimeOffset(dateTimeLocal, dateTimeLocal - dateTimeUtc); - var dateTimeOffset = new DateTimeOffset(dateTimeLocal); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("tsrange")); + Assert.That(reader[1], Is.EqualTo(@"[""1998-04-12 13:26:38"",""1998-04-12 15:26:38""]")); + } - using (var cmd = new NpgsqlCommand("SELECT @p1, @p2, @p3, @p4, @p5, @p6, @p7", conn)) + [Test] + public async Task Timestamp_multirange_resolution() + { + await using var conn = await OpenConnectionAsync(); + MinimumPgVersion(conn, "14.0", "Multirange types were introduced in PostgreSQL 14"); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) { - cmd.Parameters.AddWithValue("p1", NpgsqlDbType.TimestampTz, dateTimeUtc); - cmd.Parameters.AddWithValue("p2", NpgsqlDbType.TimestampTz, dateTimeLocal); - cmd.Parameters.AddWithValue("p3", NpgsqlDbType.TimestampTz, dateTimeUnspecified); - cmd.Parameters.AddWithValue("p4", NpgsqlDbType.TimestampTz, nDateTimeUtc); - cmd.Parameters.AddWithValue("p5", NpgsqlDbType.TimestampTz, nDateTimeLocal); - cmd.Parameters.AddWithValue("p6", NpgsqlDbType.TimestampTz, nDateTimeUnspecified); - cmd.Parameters.AddWithValue("p7", dateTimeOffset); - Assert.That(cmd.Parameters["p7"].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz)); - - using (var reader = await cmd.ExecuteReaderAsync()) + Parameters = { - reader.Read(); - - for (var i = 0; i < cmd.Parameters.Count; i++) + new() { - // Regular type (DateTime) - Assert.That(reader.GetFieldType(i), Is.EqualTo(typeof(DateTime))); - Assert.That(reader.GetDateTime(i), Is.EqualTo(dateTimeLocal)); - Assert.That(reader.GetFieldValue(i).Kind, Is.EqualTo(DateTimeKind.Local)); - Assert.That(reader[i], Is.EqualTo(dateTimeLocal)); - Assert.That(reader.GetValue(i), Is.EqualTo(dateTimeLocal)); - - // Provider-specific type (NpgsqlDateTime) - Assert.That(reader.GetTimeStamp(i), Is.EqualTo(nDateTimeLocal)); - Assert.That(reader.GetProviderSpecificFieldType(i), Is.EqualTo(typeof(NpgsqlDateTime))); - Assert.That(reader.GetProviderSpecificValue(i), Is.EqualTo(nDateTimeLocal)); - Assert.That(reader.GetFieldValue(i), Is.EqualTo(nDateTimeLocal)); - - // DateTimeOffset - Assert.That(reader.GetFieldValue(i), Is.EqualTo(dateTimeOffset)); - var x = reader.GetFieldValue(i); + Value = new[] + { + new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Local), + new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Local)), + new NpgsqlRange( + new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Local), + new DateTime(1998, 4, 13, 15, 26, 38, DateTimeKind.Local)), + } } } - } + }; + + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("tsmultirange")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Multirange | NpgsqlDbType.Timestamp)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); - Assert.AreEqual(nDateTimeUtc, nDateTimeLocal.ToUniversalTime()); - Assert.AreEqual(nDateTimeUtc, new NpgsqlDateTime(nDateTimeLocal.Ticks, DateTimeKind.Unspecified).ToUniversalTime()); - Assert.AreEqual(nDateTimeLocal, nDateTimeUnspecified.ToLocalTime()); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("tsmultirange")); + Assert.That(reader[1], Is.EqualTo(@"{[""1998-04-12 13:26:38"",""1998-04-12 15:26:38""],[""1998-04-13 13:26:38"",""1998-04-13 15:26:38""]}")); } - static readonly TestCaseData[] TimeStampTzSpecialCases = { - new TestCaseData(NpgsqlDateTime.Infinity).SetName(nameof(TimeStampTzSpecialCases) + "Infinity"), - new TestCaseData(NpgsqlDateTime.NegativeInfinity).SetName(nameof(TimeStampTzSpecialCases) + "NegativeInfinity"), - new TestCaseData(new NpgsqlDateTime(-5, 3, 3, 1, 0, 0, DateTimeKind.Local)).SetName(nameof(TimeStampTzSpecialCases) + "BC"), + #endregion + + #region Timestamp with timezone + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamptz_read(DateTime dateTime, string s) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}+00'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp with time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(DateTime))); + + Assert.That(reader[0], Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0), Is.EqualTo(dateTime)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0).Kind, Is.EqualTo(DateTimeKind.Utc)); + + // DateTimeOffset + Assert.That(reader.GetFieldValue(0), Is.EqualTo(new DateTimeOffset(dateTime))); + Assert.That(reader.GetFieldValue(0).Offset, Is.EqualTo(TimeSpan.Zero)); + + // Provider-specific type (NpgsqlTimeStamp) + var npgsqlDateTime = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Utc); + Assert.That(reader.GetProviderSpecificFieldType(0), Is.EqualTo(typeof(NpgsqlDateTime))); + Assert.That(reader.GetTimeStamp(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetProviderSpecificValue(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetTimeStamp(0).Kind, Is.EqualTo(DateTimeKind.Utc)); + } + + static readonly TestCaseData[] TimestampTzValues = + { + new TestCaseData(new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), "1998-04-12 15:26:38+02") + .SetName("TimestampTzPre2000"), + new TestCaseData(new DateTime(2015, 1, 27, 8, 45, 12, 345, DateTimeKind.Utc), "2015-01-27 09:45:12.345+01") + .SetName("TimestampTzPost2000"), + new TestCaseData(new DateTime(2013, 7, 25, 0, 0, 0, DateTimeKind.Utc), "2013-07-25 02:00:00+02") + .SetName("TimestampTzDateOnly"), + new TestCaseData(NpgsqlDateTime.Infinity, "infinity") + .SetName("TimestampTzInfinity"), + new TestCaseData(NpgsqlDateTime.NegativeInfinity, "-infinity") + .SetName("TimestampTzNegativeInfinity"), + new TestCaseData(new NpgsqlDateTime(-5, 3, 3, 1, 0, 0, DateTimeKind.Utc), "0005-03-03 01:53:28+00:53:28 BC") + .SetName("TimestampTzBC"), }; - [Test, TestCaseSource(nameof(TimeStampTzSpecialCases))] - public async Task TimeStampTzSpecial(NpgsqlDateTime value) + [Test, TestCaseSource(nameof(TimestampTzValues))] + public async Task Timestamptz_write_values(object dateTime, string expected) { - using var conn = await OpenConnectionAsync(); - using var cmd = new NpgsqlCommand("SELECT @p", conn); - cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p", Value = value, NpgsqlDbType = NpgsqlDbType.TimestampTz }); - using (var reader = await cmd.ExecuteReaderAsync()) + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) { - reader.Read(); - Assert.That(reader.GetProviderSpecificValue(0), Is.EqualTo(value)); - Assert.That(() => reader.GetDateTime(0), Throws.Exception.TypeOf()); + Parameters = { new() { Value = dateTime, NpgsqlDbType = NpgsqlDbType.TimestampTz} } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestamptzParameters + { + get + { + var dateTime = new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc); + + return new NpgsqlParameter[] + { + new() { Value = dateTime }, + new() { Value = dateTime, NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Utc), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new DateTimeOffset(dateTime) } + }; } - Assert.That(await conn.ExecuteScalarAsync("SELECT 1"), Is.EqualTo(1)); + } + + [Test, TestCaseSource(nameof(TimestamptzParameters))] + public async Task Timestamptz_resolution(NpgsqlParameter parameter) + { + if (IsMultiplexing) + return; // conn.TypeMapper.Reset + + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(parameter.DataTypeName, Is.EqualTo("timestamp with time zone")); + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz)); + Assert.That(parameter.DbType, Is.EqualTo(DbType.DateTimeOffset)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp with time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 15:26:38+02")); + } + + static NpgsqlParameter[] TimestamptzInvalidParameters + => new NpgsqlParameter[] + { + new() { Value = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = DateTime.Now, NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(0, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(0, DateTimeKind.Local), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new DateTimeOffset(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), TimeSpan.FromHours(2)) } + }; + + [Test, TestCaseSource(nameof(TimestamptzInvalidParameters))] + public async Task Timestamptz_resolution_failure(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); } [Test, Description("Makes sure that when ConvertInfinityDateTime is true, infinity values are properly converted")] @@ -430,6 +591,155 @@ public async Task TimeStampTzConvertInfinity() Assert.That(reader.GetDateTime(1), Is.EqualTo(DateTime.MinValue)); } + [Test] + public async Task Timestamptz_array_resolution() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { new() { Value = new[] { new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc) } } } + }; + + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("timestamp with time zone[]")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Array | NpgsqlDbType.TimestampTz)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp with time zone[]")); + Assert.That(reader[1], Is.EqualTo(@"{""1998-04-12 15:26:38+02""}")); + } + + [Test] + public async Task Timestamptz_range_resolution() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = + { + new() + { + Value = new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Utc)) + } + } + }; + + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("tstzrange")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Range | NpgsqlDbType.TimestampTz)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("tstzrange")); + Assert.That(reader[1], Is.EqualTo(@"[""1998-04-12 15:26:38+02"",""1998-04-12 17:26:38+02""]")); + } + + [Test] + public async Task Timestamptz_multirange_resolution() + { + await using var conn = await OpenConnectionAsync(); + MinimumPgVersion(conn, "14.0", "Multirange types were introduced in PostgreSQL 14"); + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = + { + new() + { + Value = new[] + { + new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Utc)), + new NpgsqlRange( + new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 13, 15, 26, 38, DateTimeKind.Utc)), + } + } + } + }; + + Assert.That(cmd.Parameters[0].DataTypeName, Is.EqualTo("tstzmultirange")); + Assert.That(cmd.Parameters[0].NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Multirange | NpgsqlDbType.TimestampTz)); + Assert.That(cmd.Parameters[0].DbType, Is.EqualTo(DbType.Object)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("tstzmultirange")); + Assert.That(reader[1], Is.EqualTo(@"{[""1998-04-12 15:26:38+02"",""1998-04-12 17:26:38+02""],[""1998-04-13 15:26:38+02"",""1998-04-13 17:26:38+02""]}")); + } + + [Test] + public async Task Cannot_mix_DateTime_Kinds_in_array() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1", conn) + { + Parameters = + { + new() + { + Value = new[] + { + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Local), + } + } + } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task Cannot_mix_DateTime_Kinds_in_range() + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1", conn) + { + Parameters = + { + new() + { + Value = new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Local)) + } + } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + [Test] + public async Task Cannot_mix_DateTime_Kinds_in_multirange() + { + await using var conn = await OpenConnectionAsync(); + MinimumPgVersion(conn, "14.0", "Multirange types were introduced in PostgreSQL 14"); + await using var cmd = new NpgsqlCommand("SELECT $1", conn) + { + Parameters = + { + new() + { + Value = new[] + { + new NpgsqlRange( + new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), + new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Utc)), + new NpgsqlRange( + new DateTime(1998, 4, 13, 13, 26, 38, DateTimeKind.Local), + new DateTime(1998, 4, 13, 15, 26, 38, DateTimeKind.Local)), + } + } + } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + #endregion #region Interval @@ -473,6 +783,16 @@ public async Task Interval() #endregion + protected override async ValueTask OpenConnectionAsync(string? connectionString = null) + { + var conn = await base.OpenConnectionAsync(connectionString); + await conn.ExecuteNonQueryAsync("SET TimeZone='Europe/Berlin'"); + return conn; + } + + protected override NpgsqlConnection OpenConnection(string? connectionString = null) + => throw new NotSupportedException(); + public DateTimeTests(MultiplexingMode multiplexingMode) : base(multiplexingMode) {} } } diff --git a/test/Npgsql.Tests/Types/LegacyDateTimeTests.cs b/test/Npgsql.Tests/Types/LegacyDateTimeTests.cs new file mode 100644 index 0000000000..d03db798c0 --- /dev/null +++ b/test/Npgsql.Tests/Types/LegacyDateTimeTests.cs @@ -0,0 +1,256 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using NpgsqlTypes; +using NUnit.Framework; +using static Npgsql.Util.Statics; + +namespace Npgsql.Tests.Types +{ + [NonParallelizable] + public class LegacyDateTimeTests : MultiplexingTestBase + { + static readonly TestCaseData[] TimestampValues = + { + new TestCaseData(new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), "1998-04-12 13:26:38") + .SetName("TimestampPre2000"), + new TestCaseData(new DateTime(2015, 1, 27, 8, 45, 12, 345, DateTimeKind.Utc), "2015-01-27 08:45:12.345") + .SetName("TimestampPost2000"), + new TestCaseData(new DateTime(2013, 7, 25, 0, 0, 0, DateTimeKind.Utc), "2013-07-25 00:00:00") + .SetName("TimestampDateOnly"), + }; + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_read(DateTime dateTime, string s) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}'::timestamp without time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp without time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(DateTime))); + + Assert.That(reader[0], Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0), Is.EqualTo(dateTime)); + Assert.That(reader.GetDateTime(0).Kind, Is.EqualTo(DateTimeKind.Unspecified)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateTime)); + + // Provider-specific type (NpgsqlTimeStamp) + var npgsqlDateTime = new NpgsqlDateTime(dateTime.Ticks); + Assert.That(reader.GetProviderSpecificFieldType(0), Is.EqualTo(typeof(NpgsqlDateTime))); + Assert.That(reader.GetTimeStamp(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetProviderSpecificValue(0), Is.EqualTo(npgsqlDateTime)); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(npgsqlDateTime)); + + // DateTimeOffset + Assert.That(() => reader.GetFieldValue(0), Throws.Exception.TypeOf()); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamp_write_values(DateTime dateTime, string expected) + { + Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = + { + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.Timestamp } + } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestampParameters + { + get + { + var dateTime = new DateTime(1998, 4, 12, 13, 26, 38); + + return new NpgsqlParameter[] + { + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified) }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Local) }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) }, + new() { Value = dateTime, NpgsqlDbType = NpgsqlDbType.Timestamp }, + new() { Value = dateTime, DbType = DbType.DateTime }, + new() { Value = dateTime, DbType = DbType.DateTime2 }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Unspecified) }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Local) }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Utc) }, + }; + } + } + + [Test, TestCaseSource(nameof(TimestampParameters))] + public async Task Timestamp_resolution(NpgsqlParameter parameter) + { + if (IsMultiplexing) + return; // conn.TypeMapper.Reset + + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Timestamp)); + Assert.That(parameter.DbType, Is.EqualTo(DbType.DateTime).Or.EqualTo(DbType.DateTime2)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp without time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 13:26:38")); + } + + static NpgsqlParameter[] TimestampInvalidParameters + => new NpgsqlParameter[] + { + new() { Value = new DateTimeOffset(), NpgsqlDbType = NpgsqlDbType.Timestamp } + }; + + [Test, TestCaseSource(nameof(TimestampInvalidParameters))] + public async Task Timestamp_resolution_failure(NpgsqlParameter parameter) + { + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); + } + + [Test, TestCaseSource(nameof(TimestampValues))] + public async Task Timestamptz_read(DateTime dateTime, string s) + { + Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand($"SELECT '{s}+00'::timestamp with time zone", conn); + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.That(reader.GetDataTypeName(0), Is.EqualTo("timestamp with time zone")); + Assert.That(reader.GetFieldType(0), Is.EqualTo(typeof(DateTime))); + + Assert.That(reader[0], Is.EqualTo(dateTime.ToLocalTime())); + Assert.That(reader.GetDateTime(0), Is.EqualTo(dateTime.ToLocalTime())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(dateTime.ToLocalTime())); + Assert.That(reader.GetDateTime(0).Kind, Is.EqualTo(DateTimeKind.Local)); + + // DateTimeOffset + Assert.That(reader.GetFieldValue(0), Is.EqualTo(new DateTimeOffset(dateTime.ToLocalTime()))); + + // Provider-specific type (NpgsqlTimeStamp) + var npgsqlDateTime = new NpgsqlDateTime(dateTime.Ticks); + Assert.That(reader.GetProviderSpecificFieldType(0), Is.EqualTo(typeof(NpgsqlDateTime))); + Assert.That(reader.GetTimeStamp(0), Is.EqualTo(npgsqlDateTime.ToLocalTime())); + Assert.That(reader.GetProviderSpecificValue(0), Is.EqualTo(npgsqlDateTime.ToLocalTime())); + Assert.That(reader.GetFieldValue(0), Is.EqualTo(npgsqlDateTime.ToLocalTime())); + Assert.That(reader.GetTimeStamp(0).Kind, Is.EqualTo(DateTimeKind.Local)); + } + + static readonly TestCaseData[] TimestampTzValues = + { + new TestCaseData(new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc), "1998-04-12 15:26:38+02") + .SetName("TimestampPre2000"), + new TestCaseData(new DateTime(2015, 1, 27, 8, 45, 12, 345, DateTimeKind.Utc), "2015-01-27 09:45:12.345+01") + .SetName("TimestampPost2000"), + new TestCaseData(new DateTime(2013, 7, 25, 0, 0, 0, DateTimeKind.Utc), "2013-07-25 02:00:00+02") + .SetName("TimestampDateOnly"), + }; + + [Test, TestCaseSource(nameof(TimestampTzValues))] + public async Task Timestamptz_write_values(DateTime dateTime, string expected) + { + Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc)); + + await using var conn = await OpenConnectionAsync(); + await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) + { + Parameters = { new() { Value = dateTime, NpgsqlDbType = NpgsqlDbType.TimestampTz } } + }; + + Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(expected)); + } + + static NpgsqlParameter[] TimestamptzParameters + { + get + { + var dateTime = new DateTime(1998, 4, 12, 13, 26, 38, DateTimeKind.Utc); + + return new NpgsqlParameter[] + { + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = dateTime.ToLocalTime(), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Utc).ToLocalTime(), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new NpgsqlDateTime(dateTime.Ticks, DateTimeKind.Utc), NpgsqlDbType = NpgsqlDbType.TimestampTz }, + new() { Value = new DateTimeOffset(dateTime.ToLocalTime()) } + }; + } + } + + [Test, TestCaseSource(nameof(TimestamptzParameters))] + public async Task Timestamptz_resolution(NpgsqlParameter parameter) + { + if (IsMultiplexing) + return; // conn.TypeMapper.Reset + + await using var conn = await OpenConnectionAsync(); + conn.TypeMapper.Reset(); + + await using var cmd = new NpgsqlCommand("SELECT pg_typeof($1)::text, $1::text", conn) + { + Parameters = { parameter } + }; + + Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.TimestampTz)); + Assert.That(parameter.DbType, Is.EqualTo(DbType.DateTimeOffset)); + + await using var reader = await cmd.ExecuteReaderAsync(); + await reader.ReadAsync(); + Assert.That(reader[0], Is.EqualTo("timestamp with time zone")); + Assert.That(reader[1], Is.EqualTo("1998-04-12 15:26:38+02")); + } + + protected override async ValueTask OpenConnectionAsync(string? connectionString = null) + { + var conn = await base.OpenConnectionAsync(connectionString); + await conn.ExecuteNonQueryAsync("SET TimeZone='Europe/Berlin'"); + return conn; + } + + protected override NpgsqlConnection OpenConnection(string? connectionString = null) + => throw new NotSupportedException(); + + [OneTimeSetUp] + public void Setup() + { +#if DEBUG + LegacyTimestampBehavior = true; +#else + Assert.Ignore( + "Legacy DateTime tests rely on the Npgsql.EnableLegacyTimestampBehavior AppContext switch and can only be run in DEBUG builds"); +#endif + } + + [OneTimeTearDown] + public void Teardown() + { +#if DEBUG + LegacyTimestampBehavior = false; +#endif + } + + public LegacyDateTimeTests(MultiplexingMode multiplexingMode) : base(multiplexingMode) {} + } +} From bf76fd9ca2347b09e51ecce42446222aa5a49861 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 10 Sep 2021 13:50:32 +0200 Subject: [PATCH 02/10] Generic value type writing --- .../Internal/NodaTimeTypeHandlerResolver.cs | 25 ++ .../TypeHandling/TypeHandlerResolver.cs | 2 + .../TypeMapping/BuiltInTypeHandlerResolver.cs | 256 ++++++++++++++---- src/Npgsql/TypeMapping/ConnectorTypeMapper.cs | 15 +- test/Npgsql.Benchmarks/ResolveHandler.cs | 8 +- 5 files changed, 254 insertions(+), 52 deletions(-) diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs index fdfead4c28..f00041ad8d 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs @@ -55,6 +55,31 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector) ? handler : null; + public override NpgsqlTypeHandler? ResolveValueTypeGenerically(T value) + { + if (typeof(T) == typeof(Instant)) + return LegacyTimestampBehavior ? _timestampHandler : _timestampTzHandler; + + if (typeof(T) == typeof(LocalDateTime)) + return _timestampHandler; + if (typeof(T) == typeof(ZonedDateTime)) + return _timestampTzHandler; + if (typeof(T) == typeof(OffsetDateTime)) + return _timestampTzHandler; + if (typeof(T) == typeof(LocalDate)) + return _dateHandler; + if (typeof(T) == typeof(LocalTime)) + return _timeHandler; + if (typeof(T) == typeof(OffsetTime)) + return _timeTzHandler; + if (typeof(T) == typeof(Period)) + return _intervalHandler; + if (typeof(T) == typeof(Duration)) + return _intervalHandler; + + return null; + } + internal static string? ClrTypeToDataTypeName(Type type) { if (type == typeof(Instant)) diff --git a/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs index 1b86b0f39d..411039ab2c 100644 --- a/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs +++ b/src/Npgsql/Internal/TypeHandling/TypeHandlerResolver.cs @@ -20,6 +20,8 @@ public abstract class TypeHandlerResolver public virtual NpgsqlTypeHandler? ResolveValueDependentValue(object value) => null; + public virtual NpgsqlTypeHandler? ResolveValueTypeGenerically(T value) => null; + /// /// Gets type mapping information for a given PostgreSQL type. /// Invoked in scenarios when mapping information is required, rather than a type handler for reading or writing. diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs index 6e0b1d03d5..673afbc08e 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs @@ -275,81 +275,81 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector) "smallint" => _int16Handler, "integer" or "int" => _int32Handler, "bigint" => _int64Handler, - "real" => _singleHandler ??= new SingleHandler(PgType("real")), + "real" => SingleHandler(), "double precision" => _doubleHandler, "numeric" or "decimal" => _numericHandler, - "money" => _moneyHandler ??= new MoneyHandler(PgType("money")), + "money" => MoneyHandler(), // Text types "text" => _textHandler, - "xml" => _xmlHandler ??= new TextHandler(PgType("xml"), _connector.TextEncoding), - "varchar" or "character varying" => _varcharHandler ??= new TextHandler(PgType("character varying"), _connector.TextEncoding), - "character" => _charHandler ??= new TextHandler(PgType("character"), _connector.TextEncoding), - "name" => _nameHandler ??= new TextHandler(PgType("name"), _connector.TextEncoding), - "refcursor" => _refcursorHandler ??= new TextHandler(PgType("refcursor"), _connector.TextEncoding), - "citext" => _citextHandler ??= new TextHandler(PgType("citext"), _connector.TextEncoding), + "xml" => XmlHandler(), + "varchar" or "character varying" => VarcharHandler(), + "character" => CharHandler(), + "name" => NameHandler(), + "refcursor" => RefcursorHandler(), + "citext" => CitextHandler(), "jsonb" => _jsonbHandler, - "json" => _jsonHandler ??= new JsonHandler(PgType("json"), _connector.TextEncoding, isJsonb: false), - "jsonpath" => _jsonPathHandler ??= new JsonPathHandler(PgType("jsonpath"), _connector.TextEncoding), + "json" => JsonHandler(), + "jsonpath" => JsonPathHandler(), // Date/time types "timestamp" or "timestamp without time zone" => _timestampHandler, "timestamptz" or "timestamp with time zone" => _timestampTzHandler, "date" => _dateHandler, - "time without time zone" => _timeHandler ??= new TimeHandler(PgType("time without time zone")), - "time with time zone" => _timeTzHandler ??= new TimeTzHandler(PgType("time with time zone")), - "interval" => _intervalHandler ??= new IntervalHandler(PgType("interval")), + "time without time zone" => TimeHandler(), + "time with time zone" => TimeTzHandler(), + "interval" => IntervalHandler(), // Network types - "cidr" => _cidrHandler ??= new CidrHandler(PgType("cidr")), - "inet" => _inetHandler ??= new InetHandler(PgType("inet")), - "macaddr" => _macaddrHandler ??= new MacaddrHandler(PgType("macaddr")), - "macaddr8" => _macaddr8Handler ??= new MacaddrHandler(PgType("macaddr8")), + "cidr" => CidrHandler(), + "inet" => InetHandler(), + "macaddr" => MacaddrHandler(), + "macaddr8" => Macaddr8Handler(), // Full-text search types - "tsquery" => _tsQueryHandler ??= new TsQueryHandler(PgType("tsquery")), - "tsvector" => _tsVectorHandler ??= new TsVectorHandler(PgType("tsvector")), + "tsquery" => TsQueryHandler(), + "tsvector" => TsVectorHandler(), // Geometry types - "box" => _boxHandler ??= new BoxHandler(PgType("box")), - "circle" => _circleHandler ??= new CircleHandler(PgType("circle")), - "line" => _lineHandler ??= new LineHandler(PgType("line")), - "lseg" => _lineSegmentHandler ??= new LineSegmentHandler(PgType("lseg")), - "path" => _pathHandler ??= new PathHandler(PgType("path")), - "point" => _pointHandler ??= new PointHandler(PgType("point")), - "polygon" => _polygonHandler ??= new PolygonHandler(PgType("polygon")), + "box" => BoxHandler(), + "circle" => CircleHandler(), + "line" => LineHandler(), + "lseg" => LineSegmentHandler(), + "path" => PathHandler(), + "point" => PointHandler(), + "polygon" => PolygonHandler(), // LTree types - "lquery" => _lQueryHandler ??= new LQueryHandler(PgType("lquery"), _connector.TextEncoding), - "ltree" => _lTreeHandler ??= new LTreeHandler(PgType("ltree"), _connector.TextEncoding), - "ltxtquery" => _lTxtQueryHandler ??= new LTxtQueryHandler(PgType("ltxtquery"), _connector.TextEncoding), + "lquery" => LQueryHandler(), + "ltree" => LTreeHandler(), + "ltxtquery" => LTxtHandler(), // UInt types - "oid" => _oidHandler ??= new UInt32Handler(PgType("oid")), - "xid" => _xidHandler ??= new UInt32Handler(PgType("xid")), - "xid8" => _xid8Handler ??= new UInt64Handler(PgType("xid8")), - "cid" => _cidHandler ??= new UInt32Handler(PgType("cid")), - "regtype" => _regtypeHandler ??= new UInt32Handler(PgType("regtype")), - "regconfig" => _regconfigHandler ??= new UInt32Handler(PgType("regconfig")), + "oid" => OidHandler(), + "xid" => XidHandler(), + "xid8" => Xid8Handler(), + "cid" => CidHandler(), + "regtype" => RegtypeHandler(), + "regconfig" => RegconfigHandler(), // Misc types "bool" or "boolean" => _boolHandler, - "bytea" => _byteaHandler ??= new ByteaHandler(PgType("bytea")), + "bytea" => ByteaHandler(), "uuid" => _uuidHandler, - "bit varying" or "varbit" => _bitVaryingHandler ??= new BitStringHandler(PgType("bit varying")), - "bit" => _bitHandler ??= new BitStringHandler(PgType("bit")), - "hstore" => _hstoreHandler ??= new HstoreHandler(PgType("hstore"), _textHandler), + "bit varying" or "varbit" => BitVaryingHandler(), + "bit" => BitHandler(), + "hstore" => HstoreHandler(), // Internal types - "int2vector" => _int2VectorHandler ??= new Int2VectorHandler(PgType("int2vector"), PgType("smallint")), - "oidvector" => _oidVectorHandler ??= new OIDVectorHandler(PgType("oidvector"), PgType("oid")), - "pg_lsn" => _pgLsnHandler ??= new PgLsnHandler(PgType("pg_lsn")), - "tid" => _tidHandler ??= new TidHandler(PgType("tid")), - "char" => _internalCharHandler ??= new InternalCharHandler(PgType("char")), - "record" => _recordHandler ??= new RecordHandler(PgType("record"), _connector.TypeMapper), - "void" => _voidHandler ??= new VoidHandler(PgType("void")), + "int2vector" => Int2VectorHandler(), + "oidvector" => OidVectorHandler(), + "pg_lsn" => PgLsnHandler(), + "tid" => TidHandler(), + "char" => InternalCharHandler(), + "record" => RecordHandler(), + "void" => VoidHandler(), - "unknown" => _unknownHandler ??= new UnknownTypeHandler(_connector), + "unknown" => UnknownHandler(), _ => null }; @@ -523,6 +523,96 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) }; } + public override NpgsqlTypeHandler? ResolveValueTypeGenerically(T value) + { + // This method only ever gets called for value types. + + // Numeric types + if (typeof(T) == typeof(byte)) + return _int16Handler; + if (typeof(T) == typeof(short)) + return _int16Handler; + if (typeof(T) == typeof(int)) + return _int32Handler; + if (typeof(T) == typeof(long)) + return _int64Handler; + if (typeof(T) == typeof(float)) + return SingleHandler(); + if (typeof(T) == typeof(double)) + return _doubleHandler; + if (typeof(T) == typeof(decimal)) + return _numericHandler; + if (typeof(T) == typeof(BigInteger)) + return _numericHandler; + + // Text types + if (typeof(T) == typeof(char)) + return _textHandler; + if (typeof(T) == typeof(ArraySegment)) + return _textHandler; + if (typeof(T) == typeof(JsonDocument)) + return _jsonbHandler; + + // Date/time types + // No resolution for DateTime, since that's value-dependent (Kind) + if (typeof(T) == typeof(DateTimeOffset)) + return _timestampTzHandler; + if (typeof(T) == typeof(NpgsqlDate)) + return _dateHandler; +#if NET6_0_OR_GREATER + if (typeof(T) == typeof(DateOnly)) + return _dateHandler; + if (typeof(T) == typeof(TimeOnly)) + return _timeHandler; +#endif + if (typeof(T) == typeof(TimeSpan)) + return _intervalHandler; + if (typeof(T) == typeof(NpgsqlTimeSpan)) + return _intervalHandler; + + // Network types + if (typeof(T) == typeof(IPAddress)) + return InetHandler(); + if (typeof(T) == typeof(PhysicalAddress)) + return _macaddrHandler; + if (typeof(T) == typeof(TimeSpan)) + return _intervalHandler; + + // Geometry types + if (typeof(T) == typeof(NpgsqlBox)) + return BoxHandler(); + if (typeof(T) == typeof(NpgsqlCircle)) + return CircleHandler(); + if (typeof(T) == typeof(NpgsqlLine)) + return LineHandler(); + if (typeof(T) == typeof(NpgsqlLSeg)) + return LineSegmentHandler(); + if (typeof(T) == typeof(NpgsqlPath)) + return PathHandler(); + if (typeof(T) == typeof(NpgsqlPoint)) + return PointHandler(); + if (typeof(T) == typeof(NpgsqlPolygon)) + return PolygonHandler(); + + // Misc types + if (typeof(T) == typeof(bool)) + return _boolHandler; + if (typeof(T) == typeof(Guid)) + return _uuidHandler; + if (typeof(T) == typeof(BitVector32)) + return BitVaryingHandler(); + + // Internal types + if (typeof(T) == typeof(NpgsqlLogSequenceNumber)) + return PgLsnHandler(); + if (typeof(T) == typeof(NpgsqlTid)) + return TidHandler(); + if (typeof(T) == typeof(DBNull)) + return UnknownHandler(); + + return null; + } + internal static string? ClrTypeToDataTypeName(Type type) => ClrTypeToDataTypeNameTable.TryGetValue(type, out var dataTypeName) ? dataTypeName : null; @@ -533,5 +623,77 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) => Mappings.TryGetValue(dataTypeName, out var mapping) ? mapping : null; PostgresType PgType(string pgTypeName) => _databaseInfo.GetPostgresTypeByName(pgTypeName); + + #region Handler accessors + + // Numeric types + NpgsqlTypeHandler SingleHandler() => _singleHandler ??= new SingleHandler(PgType("real")); + NpgsqlTypeHandler MoneyHandler() => _moneyHandler ??= new MoneyHandler(PgType("money")); + + // Text types + NpgsqlTypeHandler XmlHandler() => _xmlHandler ??= new TextHandler(PgType("xml"), _connector.TextEncoding); + NpgsqlTypeHandler VarcharHandler() => _varcharHandler ??= new TextHandler(PgType("character varying"), _connector.TextEncoding); + NpgsqlTypeHandler CharHandler() => _charHandler ??= new TextHandler(PgType("character"), _connector.TextEncoding); + NpgsqlTypeHandler NameHandler() => _nameHandler ??= new TextHandler(PgType("name"), _connector.TextEncoding); + NpgsqlTypeHandler RefcursorHandler() => _refcursorHandler ??= new TextHandler(PgType("refcursor"), _connector.TextEncoding); + NpgsqlTypeHandler CitextHandler() => _citextHandler ??= new TextHandler(PgType("citext"), _connector.TextEncoding); + NpgsqlTypeHandler JsonHandler() => _jsonHandler ??= new JsonHandler(PgType("json"), _connector.TextEncoding, isJsonb: false); + NpgsqlTypeHandler JsonPathHandler() => _jsonPathHandler ??= new JsonPathHandler(PgType("jsonpath"), _connector.TextEncoding); + + // Date/time types + NpgsqlTypeHandler TimeHandler() => _timeHandler ??= new TimeHandler(PgType("time without time zone")); + NpgsqlTypeHandler TimeTzHandler() => _timeTzHandler ??= new TimeTzHandler(PgType("time with time zone")); + NpgsqlTypeHandler IntervalHandler() => _intervalHandler ??= new IntervalHandler(PgType("interval")); + + // Network types + NpgsqlTypeHandler CidrHandler() => _cidrHandler ??= new CidrHandler(PgType("cidr")); + NpgsqlTypeHandler InetHandler() => _inetHandler ??= new InetHandler(PgType("inet")); + NpgsqlTypeHandler MacaddrHandler() => _macaddrHandler ??= new MacaddrHandler(PgType("macaddr")); + NpgsqlTypeHandler Macaddr8Handler() => _macaddr8Handler ??= new MacaddrHandler(PgType("macaddr8")); + + // Full-text search types + NpgsqlTypeHandler TsQueryHandler() => _tsQueryHandler ??= new TsQueryHandler(PgType("tsquery")); + NpgsqlTypeHandler TsVectorHandler() => _tsVectorHandler ??= new TsVectorHandler(PgType("tsvector")); + + // Geometry types + NpgsqlTypeHandler BoxHandler() => _boxHandler ??= new BoxHandler(PgType("box")); + NpgsqlTypeHandler CircleHandler() => _circleHandler ??= new CircleHandler(PgType("circle")); + NpgsqlTypeHandler LineHandler() => _lineHandler ??= new LineHandler(PgType("line")); + NpgsqlTypeHandler LineSegmentHandler() => _lineSegmentHandler ??= new LineSegmentHandler(PgType("lseg")); + NpgsqlTypeHandler PathHandler() => _pathHandler ??= new PathHandler(PgType("path")); + NpgsqlTypeHandler PointHandler() => _pointHandler ??= new PointHandler(PgType("point")); + NpgsqlTypeHandler PolygonHandler() => _polygonHandler ??= new PolygonHandler(PgType("polygon")); + + // LTree types + NpgsqlTypeHandler LQueryHandler() => _lQueryHandler ??= new LQueryHandler(PgType("lquery"), _connector.TextEncoding); + NpgsqlTypeHandler LTreeHandler() => _lTreeHandler ??= new LTreeHandler(PgType("ltree"), _connector.TextEncoding); + NpgsqlTypeHandler LTxtHandler() => _lTxtQueryHandler ??= new LTxtQueryHandler(PgType("ltxtquery"), _connector.TextEncoding); + + // UInt types + NpgsqlTypeHandler OidHandler() => _oidHandler ??= new UInt32Handler(PgType("oid")); + NpgsqlTypeHandler XidHandler() => _xidHandler ??= new UInt32Handler(PgType("xid")); + NpgsqlTypeHandler Xid8Handler() => _xid8Handler ??= new UInt64Handler(PgType("xid8")); + NpgsqlTypeHandler CidHandler() => _cidHandler ??= new UInt32Handler(PgType("cid")); + NpgsqlTypeHandler RegtypeHandler() => _regtypeHandler ??= new UInt32Handler(PgType("regtype")); + NpgsqlTypeHandler RegconfigHandler() => _regconfigHandler ??= new UInt32Handler(PgType("regconfig")); + + // Misc types + NpgsqlTypeHandler ByteaHandler() => _byteaHandler ??= new ByteaHandler(PgType("bytea")); + NpgsqlTypeHandler BitVaryingHandler() => _bitVaryingHandler ??= new BitStringHandler(PgType("bit varying")); + NpgsqlTypeHandler BitHandler() => _bitHandler ??= new BitStringHandler(PgType("bit")); + NpgsqlTypeHandler HstoreHandler() => _hstoreHandler ??= new HstoreHandler(PgType("hstore"), _textHandler); + + // Internal types + NpgsqlTypeHandler Int2VectorHandler() => _int2VectorHandler ??= new Int2VectorHandler(PgType("int2vector"), PgType("smallint")); + NpgsqlTypeHandler OidVectorHandler() => _oidVectorHandler ??= new OIDVectorHandler(PgType("oidvector"), PgType("oid")); + NpgsqlTypeHandler PgLsnHandler() => _pgLsnHandler ??= new PgLsnHandler(PgType("pg_lsn")); + NpgsqlTypeHandler TidHandler() => _tidHandler ??= new TidHandler(PgType("tid")); + NpgsqlTypeHandler InternalCharHandler() => _internalCharHandler ??= new InternalCharHandler(PgType("char")); + NpgsqlTypeHandler RecordHandler() => _recordHandler ??= new RecordHandler(PgType("record"), _connector.TypeMapper); + NpgsqlTypeHandler VoidHandler() => _voidHandler ??= new VoidHandler(PgType("void")); + + NpgsqlTypeHandler UnknownHandler() => _unknownHandler ??= new UnknownTypeHandler(_connector); + + #endregion Handler accessors } } diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs index e239444ebd..1bd8fe77f4 100644 --- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs +++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs @@ -225,7 +225,20 @@ internal NpgsqlTypeHandler ResolveByValue(T value) if (value is null) return ResolveByClrType(typeof(T)); - // TODO: do better + if (typeof(T).IsValueType) + { + // Attempt to resolve value types generically via the resolver. This is the efficient fast-path, where we don't even need to + // do a dictionary lookup (the JIT elides type checks in generic methods for value types) + NpgsqlTypeHandler? handler; + foreach (var resolver in _resolvers) + if ((handler = resolver.ResolveValueTypeGenerically(value)) is not null) + return handler; + + // There may still be some value types not resolved by the above, e.g. NpgsqlRange + } + + // Value types would have been resolved above, so this is a reference type - no JIT optimizations. + // We go through the regular logic (and there's no boxing). return ResolveByValue((object)value); } diff --git a/test/Npgsql.Benchmarks/ResolveHandler.cs b/test/Npgsql.Benchmarks/ResolveHandler.cs index 79d9d2ba48..35a9c5ec38 100644 --- a/test/Npgsql.Benchmarks/ResolveHandler.cs +++ b/test/Npgsql.Benchmarks/ResolveHandler.cs @@ -42,12 +42,12 @@ public NpgsqlTypeHandler ResolveDataTypeName() => _typeMapper.ResolveByDataTypeName("integer"); [Benchmark] - public NpgsqlTypeHandler ResolveClrTypeInt() - => _typeMapper.ResolveByClrType(typeof(int)); + public NpgsqlTypeHandler ResolveClrTypeNonGeneric() + => _typeMapper.ResolveByValue((object)8); [Benchmark] - public NpgsqlTypeHandler ResolveClrTypeTid() - => _typeMapper.ResolveByClrType(typeof(NpgsqlTid)); + public NpgsqlTypeHandler ResolveClrTypeGeneric() + => _typeMapper.ResolveByValue(8); } } From 55062430850fb42a9c1363c6f835c2e51a87b4d8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 10 Sep 2021 15:10:51 +0200 Subject: [PATCH 03/10] Tiny fix to mappings in CLR --- .../TypeMapping/BuiltInTypeHandlerResolver.cs | 4 +-- src/Npgsql/TypeMapping/ConnectorTypeMapper.cs | 2 +- test/Npgsql.NodaTime.Tests/NodaTimeTests.cs | 27 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs index 673afbc08e..d47dbf65cb 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs @@ -59,8 +59,8 @@ class BuiltInTypeHandlerResolver : TypeHandlerResolver { "jsonpath", new(NpgsqlDbType.JsonPath, DbType.Object, "jsonpath") }, // Date/time types - { "timestamp without time zone", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(NpgsqlDateTime), typeof(DateTime)) }, - { "timestamp", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(DateTimeOffset)) }, + { "timestamp without time zone", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(DateTime), typeof(NpgsqlDateTime)) }, + { "timestamp", new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone", typeof(DateTime), typeof(NpgsqlDateTime)) }, { "timestamp with time zone", new(NpgsqlDbType.TimestampTz, DbType.DateTimeOffset, "timestamp with time zone", typeof(DateTimeOffset)) }, { "timestamptz", new(NpgsqlDbType.TimestampTz, DbType.DateTimeOffset, "timestamp with time zone", typeof(DateTimeOffset)) }, { "date", new(NpgsqlDbType.Date, DbType.Date, "date", typeof(NpgsqlDate) diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs index 1bd8fe77f4..0cf44fcd4f 100644 --- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs +++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs @@ -261,7 +261,7 @@ internal NpgsqlTypeHandler ResolveByValue(object value) // ResolveByClrType either throws, or resolves a handler and caches it in _handlersByClrType (where it would be found above the // next time we resolve this type) - return ResolveByClrType(value.GetType()); + return ResolveByClrType(type); } // TODO: This is needed as a separate method only because of binary COPY, see #3957 diff --git a/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs index a09983455b..41f64a4606 100644 --- a/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs +++ b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs @@ -205,26 +205,25 @@ public async Task Timestamptz_resolution(NpgsqlParameter parameter) Assert.That(reader[1], Is.EqualTo("1998-04-12 15:26:38+02")); // We set TimeZone to Europe/Berlin below } - static NpgsqlParameter[] TimestamptzInvalidParameters - => new NpgsqlParameter[] - { - new() { Value = new LocalDateTime(), NpgsqlDbType = NpgsqlDbType.TimestampTz }, - new() { Value = DateTime.Now, NpgsqlDbType = NpgsqlDbType.TimestampTz }, - new() { Value = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), NpgsqlDbType = NpgsqlDbType.TimestampTz }, - new() { Value = new DateTimeOffset(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), TimeSpan.FromHours(2)), NpgsqlDbType = NpgsqlDbType.TimestampTz }, - - // We only support ZonedDateTime and OffsetDateTime in UTC - new() { Value = new LocalDateTime().InUtc().ToInstant().InZone(DateTimeZoneProviders.Tzdb["America/New_York"]), NpgsqlDbType = NpgsqlDbType.TimestampTz }, - new() { Value = new LocalDateTime().WithOffset(Offset.FromHours(1)), NpgsqlDbType = NpgsqlDbType.TimestampTz } - }; + static readonly TestCaseData[] TimestamptzInvalidParameters = + { + new TestCaseData(new LocalDateTime()).SetName("TimestamptzInvalidParameters_LocalDateTime"), + new TestCaseData(DateTime.Now).SetName("TimestamptzInvalidParameters_DateTime_Local"), + new TestCaseData(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified)).SetName("TimestamptzInvalidParameters_DateTime_Unspecified"), + new TestCaseData(new DateTimeOffset(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), TimeSpan.FromHours(2))).SetName("TimestamptzInvalidParameters_DateTimeOffset_non_UTC"), + + // We only support ZonedDateTime and OffsetDateTime in UTC + new TestCaseData(new LocalDateTime().InUtc().ToInstant().InZone(DateTimeZoneProviders.Tzdb["America/New_York"])).SetName("TimestamptzInvalidParameters_ZonedDateTime_non_UTC"), + new TestCaseData(new LocalDateTime().WithOffset(Offset.FromHours(1))).SetName("TimestamptzInvalidParameters_OffsetDateTime_non_UTC") + }; [Test, TestCaseSource(nameof(TimestamptzInvalidParameters))] - public async Task Timestamptz_resolution_failure(NpgsqlParameter parameter) + public async Task Timestamptz_resolution_failure(object value) { await using var conn = await OpenConnectionAsync(); await using var cmd = new NpgsqlCommand("SELECT $1::text", conn) { - Parameters = { parameter } + Parameters = { new() { NpgsqlDbType = NpgsqlDbType.TimestampTz, Value = value } } }; Assert.That(() => cmd.ExecuteReaderAsync(), Throws.Exception.TypeOf()); From dcb97c6d8a5bbd004f1acb21169c366e46df3d06 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 10 Sep 2021 17:04:03 +0200 Subject: [PATCH 04/10] Fix call resolution... --- .../Internal/TimestampTzHandler.cs | 2 +- test/Npgsql.NodaTime.Tests/NodaTimeTests.cs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs b/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs index 1baa3fc7e2..7db4976969 100644 --- a/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs +++ b/src/Npgsql.NodaTime/Internal/TimestampTzHandler.cs @@ -115,7 +115,7 @@ void INpgsqlSimpleTypeHandler.Write(DateTimeOffset value, Npgsql => _bclHandler.Write(value, buf, parameter); int INpgsqlSimpleTypeHandler.ValidateAndGetLength(DateTime value, NpgsqlParameter? parameter) - => _bclHandler.ValidateAndGetLength(value, parameter); + => ((INpgsqlSimpleTypeHandler)_bclHandler).ValidateAndGetLength(value, parameter); void INpgsqlSimpleTypeHandler.Write(DateTime value, NpgsqlWriteBuffer buf, NpgsqlParameter? parameter) => _bclHandler.Write(value, buf, parameter); diff --git a/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs index 41f64a4606..677c251a59 100644 --- a/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs +++ b/test/Npgsql.NodaTime.Tests/NodaTimeTests.cs @@ -207,14 +207,20 @@ public async Task Timestamptz_resolution(NpgsqlParameter parameter) static readonly TestCaseData[] TimestamptzInvalidParameters = { - new TestCaseData(new LocalDateTime()).SetName("TimestamptzInvalidParameters_LocalDateTime"), - new TestCaseData(DateTime.Now).SetName("TimestamptzInvalidParameters_DateTime_Local"), - new TestCaseData(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified)).SetName("TimestamptzInvalidParameters_DateTime_Unspecified"), - new TestCaseData(new DateTimeOffset(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified), TimeSpan.FromHours(2))).SetName("TimestamptzInvalidParameters_DateTimeOffset_non_UTC"), + new TestCaseData(new LocalDateTime()) + .SetName("TimestamptzInvalidParameters_LocalDateTime"), + new TestCaseData(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Local)) + .SetName("TimestamptzInvalidParameters_DateTime_Local"), + new TestCaseData(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Unspecified)) + .SetName("TimestamptzInvalidParameters_DateTime_Unspecified"), + new TestCaseData(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Unspecified), TimeSpan.FromHours(2))) + .SetName("TimestamptzInvalidParameters_DateTimeOffset_non_UTC"), // We only support ZonedDateTime and OffsetDateTime in UTC - new TestCaseData(new LocalDateTime().InUtc().ToInstant().InZone(DateTimeZoneProviders.Tzdb["America/New_York"])).SetName("TimestamptzInvalidParameters_ZonedDateTime_non_UTC"), - new TestCaseData(new LocalDateTime().WithOffset(Offset.FromHours(1))).SetName("TimestamptzInvalidParameters_OffsetDateTime_non_UTC") + new TestCaseData(new LocalDateTime().InUtc().ToInstant().InZone(DateTimeZoneProviders.Tzdb["America/New_York"])) + .SetName("TimestamptzInvalidParameters_ZonedDateTime_non_UTC"), + new TestCaseData(new LocalDateTime().WithOffset(Offset.FromHours(1))) + .SetName("TimestamptzInvalidParameters_OffsetDateTime_non_UTC") }; [Test, TestCaseSource(nameof(TimestamptzInvalidParameters))] From 230a86fe9544c57c39653f9ad8613b8c07d8e76f Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 10 Sep 2021 17:11:38 +0200 Subject: [PATCH 05/10] Add note on mixing Kinds --- .../Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs | 2 ++ .../TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs index 5dc07dddf9..7e784bc5fa 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs @@ -54,6 +54,7 @@ public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parame { throw new InvalidCastException( "Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } @@ -67,6 +68,7 @@ public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? { throw new InvalidCastException( "Cannot write NpgsqlDateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs index b90aacab46..8f884426a2 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampTzHandler.cs @@ -103,6 +103,7 @@ public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parame { throw new InvalidCastException( $"Cannot write DateTime with Kind={value.Kind} to PostgreSQL type 'timestamp with time zone', only UTC is supported. " + + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } @@ -116,6 +117,7 @@ public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? { throw new InvalidCastException( $"Cannot write DateTime with Kind={value.Kind} to PostgreSQL type 'timestamp with time zone', only UTC is supported. " + + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } @@ -129,6 +131,7 @@ public int ValidateAndGetLength(DateTimeOffset value, NpgsqlParameter? parameter { throw new InvalidCastException( $"Cannot write DateTimeOffset with Offset={value.Offset} to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. " + + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } From 68866ead04273433c61b45d2c412d471fc837253 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 12 Sep 2021 07:35:49 +0200 Subject: [PATCH 06/10] Expose user type mapping info, e.g. for EF Core --- .../Internal/NodaTimeTypeHandlerResolver.cs | 3 +++ src/Npgsql/Internal/TypeHandlers/EnumHandler.cs | 12 ------------ .../{ => Internal}/TypeMapping/IUserTypeMapping.cs | 5 ++--- .../TypeMapping/UserCompositeTypeMappings.cs | 12 ++++++++---- .../TypeMapping/UserEnumTypeMappings.cs | 13 +++++++++---- .../TypeMapping/BuiltInTypeHandlerResolver.cs | 4 ++-- src/Npgsql/TypeMapping/ConnectorTypeMapper.cs | 1 + src/Npgsql/TypeMapping/GlobalTypeMapper.cs | 3 ++- 8 files changed, 27 insertions(+), 26 deletions(-) rename src/Npgsql/{ => Internal}/TypeMapping/IUserTypeMapping.cs (76%) rename src/Npgsql/{ => Internal}/TypeMapping/UserCompositeTypeMappings.cs (73%) rename src/Npgsql/{ => Internal}/TypeMapping/UserEnumTypeMappings.cs (79%) diff --git a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs index f00041ad8d..50be100d63 100644 --- a/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs +++ b/src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs @@ -57,6 +57,9 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector) public override NpgsqlTypeHandler? ResolveValueTypeGenerically(T value) { + // This method only ever gets called for value types, and relies on the JIT specializing the method for T by eliding all the + // type checks below. + if (typeof(T) == typeof(Instant)) return LegacyTimestampBehavior ? _timestampHandler : _timestampTzHandler; diff --git a/src/Npgsql/Internal/TypeHandlers/EnumHandler.cs b/src/Npgsql/Internal/TypeHandlers/EnumHandler.cs index 4e6d33f912..cf4749d85e 100644 --- a/src/Npgsql/Internal/TypeHandlers/EnumHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/EnumHandler.cs @@ -72,16 +72,4 @@ public override void Write(TEnum value, NpgsqlWriteBuffer buf, NpgsqlParameter? #endregion } - - /// - /// Interface implemented by all enum handler factories. - /// Used to expose the name translator for those reflecting enum mappings (e.g. EF Core). - /// - public interface IEnumTypeHandlerFactory - { - /// - /// The name translator used for this enum. - /// - INpgsqlNameTranslator NameTranslator { get; } - } } diff --git a/src/Npgsql/TypeMapping/IUserTypeMapping.cs b/src/Npgsql/Internal/TypeMapping/IUserTypeMapping.cs similarity index 76% rename from src/Npgsql/TypeMapping/IUserTypeMapping.cs rename to src/Npgsql/Internal/TypeMapping/IUserTypeMapping.cs index e4a7aaa5f9..253e38ba04 100644 --- a/src/Npgsql/TypeMapping/IUserTypeMapping.cs +++ b/src/Npgsql/Internal/TypeMapping/IUserTypeMapping.cs @@ -1,11 +1,10 @@ using System; -using Npgsql.Internal; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; -namespace Npgsql.TypeMapping +namespace Npgsql.Internal.TypeMapping { - interface IUserTypeMapping + public interface IUserTypeMapping { public string PgTypeName { get; } public Type ClrType { get; } diff --git a/src/Npgsql/TypeMapping/UserCompositeTypeMappings.cs b/src/Npgsql/Internal/TypeMapping/UserCompositeTypeMappings.cs similarity index 73% rename from src/Npgsql/TypeMapping/UserCompositeTypeMappings.cs rename to src/Npgsql/Internal/TypeMapping/UserCompositeTypeMappings.cs index 86ee0f6416..70cc75f883 100644 --- a/src/Npgsql/TypeMapping/UserCompositeTypeMappings.cs +++ b/src/Npgsql/Internal/TypeMapping/UserCompositeTypeMappings.cs @@ -1,16 +1,20 @@ using System; -using Npgsql.Internal; using Npgsql.Internal.TypeHandlers.CompositeHandlers; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; -namespace Npgsql.TypeMapping +namespace Npgsql.Internal.TypeMapping { - class UserCompositeTypeMapping : IUserTypeMapping + public interface IUserCompositeTypeMapping : IUserTypeMapping + { + INpgsqlNameTranslator NameTranslator { get; } + } + + class UserCompositeTypeMapping : IUserCompositeTypeMapping { public string PgTypeName { get; } public Type ClrType => typeof(T); - INpgsqlNameTranslator NameTranslator { get; } + public INpgsqlNameTranslator NameTranslator { get; } public UserCompositeTypeMapping(string pgTypeName, INpgsqlNameTranslator nameTranslator) => (PgTypeName, NameTranslator) = (pgTypeName, nameTranslator); diff --git a/src/Npgsql/TypeMapping/UserEnumTypeMappings.cs b/src/Npgsql/Internal/TypeMapping/UserEnumTypeMappings.cs similarity index 79% rename from src/Npgsql/TypeMapping/UserEnumTypeMappings.cs rename to src/Npgsql/Internal/TypeMapping/UserEnumTypeMappings.cs index f67b0b9bb2..5afa79c1de 100644 --- a/src/Npgsql/TypeMapping/UserEnumTypeMappings.cs +++ b/src/Npgsql/Internal/TypeMapping/UserEnumTypeMappings.cs @@ -2,26 +2,31 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using NpgsqlTypes; -namespace Npgsql.TypeMapping +namespace Npgsql.Internal.TypeMapping { - class UserEnumTypeMapping : IUserTypeMapping + public interface IUserEnumTypeMapping : IUserTypeMapping + { + INpgsqlNameTranslator NameTranslator { get; } + } + + class UserEnumTypeMapping : IUserEnumTypeMapping where TEnum : struct, Enum { public string PgTypeName { get; } public Type ClrType => typeof(TEnum); + public INpgsqlNameTranslator NameTranslator { get; } readonly Dictionary _enumToLabel = new(); readonly Dictionary _labelToEnum = new(); public UserEnumTypeMapping(string pgTypeName, INpgsqlNameTranslator nameTranslator) { - PgTypeName = pgTypeName; + (PgTypeName, NameTranslator) = (pgTypeName, nameTranslator); foreach (var field in typeof(TEnum).GetFields(BindingFlags.Static | BindingFlags.Public)) { diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs index d47dbf65cb..84f9f1da6b 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Specialized; @@ -525,7 +524,8 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) public override NpgsqlTypeHandler? ResolveValueTypeGenerically(T value) { - // This method only ever gets called for value types. + // This method only ever gets called for value types, and relies on the JIT specializing the method for T by eliding all the + // type checks below. // Numeric types if (typeof(T) == typeof(byte)) diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs index 0cf44fcd4f..64d896061f 100644 --- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs +++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs @@ -11,6 +11,7 @@ using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandling; +using Npgsql.Internal.TypeMapping; using Npgsql.PostgresTypes; using NpgsqlTypes; diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs index 77a8d913d3..9d4916a317 100644 --- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs +++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Threading; using Npgsql.Internal.TypeHandling; +using Npgsql.Internal.TypeMapping; using Npgsql.NameTranslation; using NpgsqlTypes; @@ -16,7 +17,7 @@ sealed class GlobalTypeMapper : TypeMapperBase public static GlobalTypeMapper Instance { get; } internal List ResolverFactories { get; } = new(); - internal Dictionary UserTypeMappings { get; } = new(); + public Dictionary UserTypeMappings { get; } = new(); /// /// A counter that is incremented whenever a global mapping change occurs. From 44fed8a17f9ee8adf42cd7e6a2466b1d46e8a10c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 12 Sep 2021 09:38:38 +0200 Subject: [PATCH 07/10] Properly handle missing types in PG And add guards against exceptions in type resolvers --- .../Internal/GeoJSONTypeHandlerResolver.cs | 10 +-- .../NetTopologySuiteTypeHandlerResolver.cs | 10 +-- src/Npgsql/Internal/NpgsqlDatabaseInfo.cs | 2 +- src/Npgsql/NpgsqlConnection.cs | 2 +- .../TypeMapping/BuiltInTypeHandlerResolver.cs | 20 ++++-- src/Npgsql/TypeMapping/ConnectorTypeMapper.cs | 72 ++++++++++++++++--- src/Npgsql/TypeMapping/GlobalTypeMapper.cs | 6 +- 7 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs index ba8e320fd3..577ba18d48 100644 --- a/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs +++ b/src/Npgsql.GeoJSON/Internal/GeoJSONTypeHandlerResolver.cs @@ -16,7 +16,7 @@ namespace Npgsql.GeoJSON.Internal public class GeoJSONTypeHandlerResolver : TypeHandlerResolver { readonly NpgsqlDatabaseInfo _databaseInfo; - readonly GeoJsonHandler _geometryHandler, _geographyHandler; + readonly GeoJsonHandler? _geometryHandler, _geographyHandler; readonly bool _geographyAsDefault; static readonly ConcurrentDictionary CRSMaps = new(); @@ -47,8 +47,10 @@ internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions op var (pgGeometryType, pgGeographyType) = (PgType("geometry"), PgType("geography")); - _geometryHandler = new GeoJsonHandler(pgGeometryType, options, crsMap); - _geographyHandler = new GeoJsonHandler(pgGeographyType, options, crsMap); + if (pgGeometryType is not null) + _geometryHandler = new GeoJsonHandler(pgGeometryType, options, crsMap); + if (pgGeographyType is not null) + _geographyHandler = new GeoJsonHandler(pgGeographyType, options, crsMap); } public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) @@ -82,6 +84,6 @@ internal GeoJSONTypeHandlerResolver(NpgsqlConnector connector, GeoJSONOptions op _ => null }; - PostgresType PgType(string pgTypeName) => _databaseInfo.GetPostgresTypeByName(pgTypeName); + PostgresType? PgType(string pgTypeName) => _databaseInfo.TryGetPostgresTypeByName(pgTypeName, out var pgType) ? pgType : null; } } diff --git a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs index 953d08e02d..1e64c82ffa 100644 --- a/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs +++ b/src/Npgsql.NetTopologySuite/Internal/NetTopologySuiteTypeHandlerResolver.cs @@ -15,7 +15,7 @@ public class NetTopologySuiteTypeHandlerResolver : TypeHandlerResolver readonly NpgsqlDatabaseInfo _databaseInfo; readonly bool _geographyAsDefault; - readonly NetTopologySuiteHandler _geometryHandler, _geographyHandler; + readonly NetTopologySuiteHandler? _geometryHandler, _geographyHandler; internal NetTopologySuiteTypeHandlerResolver( NpgsqlConnector connector, @@ -33,8 +33,10 @@ internal NetTopologySuiteTypeHandlerResolver( var reader = new PostGisReader(coordinateSequenceFactory, precisionModel, handleOrdinates); var writer = new PostGisWriter(); - _geometryHandler = new NetTopologySuiteHandler(pgGeometryType, reader, writer); - _geographyHandler = new NetTopologySuiteHandler(pgGeographyType, reader, writer); + if (pgGeometryType is not null) + _geometryHandler = new NetTopologySuiteHandler(pgGeometryType, reader, writer); + if (pgGeographyType is not null) + _geographyHandler = new NetTopologySuiteHandler(pgGeographyType, reader, writer); } public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName) @@ -68,6 +70,6 @@ internal NetTopologySuiteTypeHandlerResolver( _ => null }; - PostgresType PgType(string pgTypeName) => _databaseInfo.GetPostgresTypeByName(pgTypeName); + PostgresType? PgType(string pgTypeName) => _databaseInfo.TryGetPostgresTypeByName(pgTypeName, out var pgType) ? pgType : null; } } diff --git a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs index 0f4b717c74..ca4a120c08 100644 --- a/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs +++ b/src/Npgsql/Internal/NpgsqlDatabaseInfo.cs @@ -187,7 +187,7 @@ public PostgresType GetPostgresTypeByName(string pgName) ? pgType : throw new ArgumentException($"A PostgreSQL type with the name {pgName} was not found in the database"); - internal bool TryGetPostgresTypeByName(string pgName, [NotNullWhen(true)] out PostgresType? pgType) + public bool TryGetPostgresTypeByName(string pgName, [NotNullWhen(true)] out PostgresType? pgType) { // Full type name with namespace if (pgName.IndexOf('.') > -1) diff --git a/src/Npgsql/NpgsqlConnection.cs b/src/Npgsql/NpgsqlConnection.cs index a552dd75b6..95d0b8259b 100644 --- a/src/Npgsql/NpgsqlConnection.cs +++ b/src/Npgsql/NpgsqlConnection.cs @@ -319,7 +319,7 @@ async Task OpenAsync(bool async, CancellationToken cancellationToken) connector = await _pool.Get(this, timeout, async, cancellationToken); Debug.Assert(connector.Connection is null, - $"Connection for opened connector {Connector} is bound to another connection"); + $"Connection for opened connector '{Connector?.Id.ToString() ?? "???"}' is bound to another connection"); ConnectorBindingScope = ConnectorBindingScope.Connection; connector.Connection = this; diff --git a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs index 84f9f1da6b..83485d9009 100644 --- a/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs +++ b/src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs @@ -636,7 +636,9 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) NpgsqlTypeHandler CharHandler() => _charHandler ??= new TextHandler(PgType("character"), _connector.TextEncoding); NpgsqlTypeHandler NameHandler() => _nameHandler ??= new TextHandler(PgType("name"), _connector.TextEncoding); NpgsqlTypeHandler RefcursorHandler() => _refcursorHandler ??= new TextHandler(PgType("refcursor"), _connector.TextEncoding); - NpgsqlTypeHandler CitextHandler() => _citextHandler ??= new TextHandler(PgType("citext"), _connector.TextEncoding); + NpgsqlTypeHandler? CitextHandler() => _citextHandler ??= _databaseInfo.TryGetPostgresTypeByName("citext", out var pgType) + ? new TextHandler(pgType, _connector.TextEncoding) + : null; NpgsqlTypeHandler JsonHandler() => _jsonHandler ??= new JsonHandler(PgType("json"), _connector.TextEncoding, isJsonb: false); NpgsqlTypeHandler JsonPathHandler() => _jsonPathHandler ??= new JsonPathHandler(PgType("jsonpath"), _connector.TextEncoding); @@ -665,9 +667,15 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) NpgsqlTypeHandler PolygonHandler() => _polygonHandler ??= new PolygonHandler(PgType("polygon")); // LTree types - NpgsqlTypeHandler LQueryHandler() => _lQueryHandler ??= new LQueryHandler(PgType("lquery"), _connector.TextEncoding); - NpgsqlTypeHandler LTreeHandler() => _lTreeHandler ??= new LTreeHandler(PgType("ltree"), _connector.TextEncoding); - NpgsqlTypeHandler LTxtHandler() => _lTxtQueryHandler ??= new LTxtQueryHandler(PgType("ltxtquery"), _connector.TextEncoding); + NpgsqlTypeHandler? LQueryHandler() => _lQueryHandler ??= _databaseInfo.TryGetPostgresTypeByName("lquery", out var pgType) + ? new LQueryHandler(pgType, _connector.TextEncoding) + : null; + NpgsqlTypeHandler? LTreeHandler() => _lTreeHandler ??= _databaseInfo.TryGetPostgresTypeByName("ltree", out var pgType) + ? new LTreeHandler(pgType, _connector.TextEncoding) + : null; + NpgsqlTypeHandler? LTxtHandler() => _lTxtQueryHandler ??= _databaseInfo.TryGetPostgresTypeByName("ltxtquery", out var pgType) + ? new LTxtQueryHandler(pgType, _connector.TextEncoding) + : null; // UInt types NpgsqlTypeHandler OidHandler() => _oidHandler ??= new UInt32Handler(PgType("oid")); @@ -681,7 +689,9 @@ static DateTimeKind GetMultirangeKind(NpgsqlRange[] multirange) NpgsqlTypeHandler ByteaHandler() => _byteaHandler ??= new ByteaHandler(PgType("bytea")); NpgsqlTypeHandler BitVaryingHandler() => _bitVaryingHandler ??= new BitStringHandler(PgType("bit varying")); NpgsqlTypeHandler BitHandler() => _bitHandler ??= new BitStringHandler(PgType("bit")); - NpgsqlTypeHandler HstoreHandler() => _hstoreHandler ??= new HstoreHandler(PgType("hstore"), _textHandler); + NpgsqlTypeHandler? HstoreHandler() => _hstoreHandler ??= _databaseInfo.TryGetPostgresTypeByName("hstore", out var pgType) + ? new HstoreHandler(pgType, _textHandler) + : null; // Internal types NpgsqlTypeHandler Int2VectorHandler() => _int2VectorHandler ??= new Int2VectorHandler(PgType("int2vector"), PgType("smallint")); diff --git a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs index 64d896061f..ee8a442520 100644 --- a/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs +++ b/src/Npgsql/TypeMapping/ConnectorTypeMapper.cs @@ -7,11 +7,11 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading; using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandling; using Npgsql.Internal.TypeMapping; +using Npgsql.Logging; using Npgsql.PostgresTypes; using NpgsqlTypes; @@ -51,6 +51,8 @@ internal NpgsqlDatabaseInfo DatabaseInfo /// internal int ChangeCounter { get; private set; } + static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(ConnectorTypeMapper)); + #region Construction internal ConnectorTypeMapper(NpgsqlConnector connector) : base(GlobalTypeMapper.Instance.DefaultNameTranslator) @@ -146,9 +148,20 @@ internal NpgsqlTypeHandler ResolveByNpgsqlDbType(NpgsqlDbType npgsqlDbType) bool TryResolve(NpgsqlDbType npgsqlDbType, [NotNullWhen(true)] out NpgsqlTypeHandler? handler) { if (GlobalTypeMapper.NpgsqlDbTypeToDataTypeName(npgsqlDbType) is { } dataTypeName) + { foreach (var resolver in _resolvers) - if ((handler = resolver.ResolveByDataTypeName(dataTypeName)) is not null) - return true; + { + try + { + if ((handler = resolver.ResolveByDataTypeName(dataTypeName)) is not null) + return true; + } + catch (Exception e) + { + Log.Error($"Type resolver {resolver.GetType().Name} threw exception while resolving NpgsqlDbType {npgsqlDbType}", e); + } + } + } handler = null; return false; @@ -167,8 +180,17 @@ internal NpgsqlTypeHandler ResolveByDataTypeName(string typeName) lock (_writeLock) { foreach (var resolver in _resolvers) - if ((handler = resolver.ResolveByDataTypeName(typeName)) is not null) - return _handlersByDataTypeName[typeName] = handler; + { + try + { + if ((handler = resolver.ResolveByDataTypeName(typeName)) is not null) + return _handlersByDataTypeName[typeName] = handler; + } + catch (Exception e) + { + Log.Error($"Type resolver {resolver.GetType().Name} threw exception while resolving data type name {typeName}", e); + } + } if (DatabaseInfo.GetPostgresTypeByName(typeName) is not { } pgType) throw new NotSupportedException("Could not find PostgreSQL type " + typeName); @@ -231,9 +253,19 @@ internal NpgsqlTypeHandler ResolveByValue(T value) // Attempt to resolve value types generically via the resolver. This is the efficient fast-path, where we don't even need to // do a dictionary lookup (the JIT elides type checks in generic methods for value types) NpgsqlTypeHandler? handler; + foreach (var resolver in _resolvers) - if ((handler = resolver.ResolveValueTypeGenerically(value)) is not null) - return handler; + { + try + { + if ((handler = resolver.ResolveValueTypeGenerically(value)) is not null) + return handler; + } + catch (Exception e) + { + Log.Error($"Type resolver {resolver.GetType().Name} threw exception while resolving value with type {typeof(T)}", e); + } + } // There may still be some value types not resolved by the above, e.g. NpgsqlRange } @@ -257,8 +289,17 @@ internal NpgsqlTypeHandler ResolveByValue(object value) return handler; foreach (var resolver in _resolvers) - if ((handler = resolver.ResolveValueDependentValue(value)) is not null) - return handler; + { + try + { + if ((handler = resolver.ResolveValueDependentValue(value)) is not null) + return handler; + } + catch (Exception e) + { + Log.Error($"Type resolver {resolver.GetType().Name} threw exception while resolving value with type {type}", e); + } + } // ResolveByClrType either throws, or resolves a handler and caches it in _handlersByClrType (where it would be found above the // next time we resolve this type) @@ -274,8 +315,17 @@ internal NpgsqlTypeHandler ResolveByClrType(Type type) lock (_writeLock) { foreach (var resolver in _resolvers) - if ((handler = resolver.ResolveByClrType(type)) is not null) - return _handlersByClrType[type] = handler; + { + try + { + if ((handler = resolver.ResolveByClrType(type)) is not null) + return _handlersByClrType[type] = handler; + } + catch (Exception e) + { + Log.Error($"Type resolver {resolver.GetType().Name} threw exception while resolving value with type {type}", e); + } + } // Try to see if it is an array type var arrayElementType = GetArrayListElementType(type); diff --git a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs index 9d4916a317..631eb69a08 100644 --- a/src/Npgsql/TypeMapping/GlobalTypeMapper.cs +++ b/src/Npgsql/TypeMapping/GlobalTypeMapper.cs @@ -269,7 +269,8 @@ bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out TypeMappin return false; } - throw new NotSupportedException("Can't infer NpgsqlDbType for type " + clrType); + typeMapping = null; + return false; } } @@ -349,6 +350,9 @@ bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out TypeMappin NpgsqlDbType.Bit => "bit", NpgsqlDbType.Hstore => "hstore", + NpgsqlDbType.Geometry => "geometry", + NpgsqlDbType.Geography => "geography", + // Internal types NpgsqlDbType.Int2Vector => "int2vector", NpgsqlDbType.Oidvector => "oidvector", From 7558e016c709ccc7cf2a1e3f3457c2682f1ee775 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 13 Sep 2021 18:38:22 +0200 Subject: [PATCH 08/10] Update src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs Co-authored-by: Nikita Kazmin --- .../Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs index 7e784bc5fa..65579ea6fd 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs @@ -53,7 +53,7 @@ public override int ValidateAndGetLength(DateTime value, NpgsqlParameter? parame if (!LegacyTimestampBehavior && value.Kind == DateTimeKind.Utc) { throw new InvalidCastException( - "Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "Cannot write DateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', consider using 'timestamp with time zone'. " + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } From 0f75c53db59a8ddc02172e208c25a5efa6c14c75 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 13 Sep 2021 18:38:27 +0200 Subject: [PATCH 09/10] Update src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs Co-authored-by: Nikita Kazmin --- .../Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs index 65579ea6fd..007a74e53f 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/TimestampHandler.cs @@ -67,7 +67,7 @@ public override int ValidateAndGetLength(NpgsqlDateTime value, NpgsqlParameter? if (!LegacyTimestampBehavior && value.Kind == DateTimeKind.Utc) { throw new InvalidCastException( - "Cannot write NpgsqlDateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', considering using 'timestamp with time zone'. " + + "Cannot write NpgsqlDateTime with Kind=UTC to PostgreSQL type 'timestamp without time zone', consider using 'timestamp with time zone'. " + "Note that it's not possible to mix DateTimes with different Kinds in an array/range. " + "See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior."); } From a1db6518fb161bae3140019bf3315ec07e71d6e6 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 13 Sep 2021 18:52:22 +0200 Subject: [PATCH 10/10] Update src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs Co-authored-by: Nikita Kazmin --- .../Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs index c59560f6d7..06d903f2d5 100644 --- a/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs +++ b/src/Npgsql/Internal/TypeHandlers/DateTimeHandlers/DateTimeUtils.cs @@ -114,7 +114,7 @@ internal static void WriteTimestamp(NpgsqlDateTime value, NpgsqlWriteBuffer buf, else { var uSecsDate = (730119 - value.Date.DaysSinceEra) * 86400000000L; - buf.WriteInt64(-(uSecsDate - uSecsTime)); + buf.WriteInt64(uSecsTime - uSecsDate); } } }