From 6b3ca689df69e3ec9135bc14570612ff232c3792 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 18 May 2024 19:42:23 +0200 Subject: [PATCH] Major work around DbDataSource management, enum handling and plugins (#3167) Fixes #2891 Fixes #3063 Fixes #1026 Co-authored-by: Nino Floris --- EFCore.PG.sln | 15 - ...pgsqlNetTopologySuiteDesignTimeServices.cs | 2 +- ...ySuiteDbContextOptionsBuilderExtensions.cs | 23 +- ...opologySuiteServiceCollectionExtensions.cs | 8 +- .../INpgsqlNetTopologySuiteOptions.cs | 14 - ...INpgsqlNetTopologySuiteSingletonOptions.cs | 41 + ...ologySuiteDataSourceConfigurationPlugin.cs | 26 + .../NpgsqlNetTopologySuiteOptionsExtension.cs | 76 +- ...NpgsqlNetTopologySuiteSingletonOptions.cs} | 14 +- ...NetTopologySuiteTypeMappingSourcePlugin.cs | 4 +- ...daTimeDbContextOptionsBuilderExtensions.cs | 5 - ...gsqlNodaTimeServiceCollectionExtensions.cs | 5 +- .../NodaTimeDataSourceConfigurationPlugin.cs | 19 + .../Properties/AssemblyInfo.cs | 2 +- ...sqlCSharpRuntimeAnnotationCodeGenerator.cs | 36 +- .../NpgsqlModelBuilderExtensions.cs | 55 + .../NpgsqlModelExtensions.cs | 13 + .../NpgsqlServiceCollectionExtensions.cs | 3 +- .../EntityFrameworkNpgsqlServicesBuilder.cs | 36 + .../INpgsqlDataSourceConfigurationPlugin.cs | 24 + .../Infrastructure/Internal/EnumDefinition.cs | 148 ++ .../Internal/INpgsqlSingletonOptions.cs | 10 +- .../Internal/NpgsqlOptionsExtension.cs | 101 +- .../Internal/UserRangeDefinition.cs | 74 + .../NpgsqlDbContextOptionsBuilder.cs | 27 + .../Internal/NpgsqlSingletonOptions.cs | 27 +- .../Conventions/NpgsqlConventionSetBuilder.cs | 5 +- ...NpgsqlPostgresModelFinalizingConvention.cs | 26 +- src/EFCore.PG/Metadata/PostgresEnum.cs | 43 + .../NpgsqlMigrationsSqlGenerator.cs | 4 +- .../Internal/NpgsqlStringMethodTranslator.cs | 4 +- .../Mapping/NpgsqlArrayTypeMapping.cs | 20 +- .../Internal/Mapping/NpgsqlEnumTypeMapping.cs | 84 +- .../Mapping/NpgsqlRangeTypeMapping.cs | 8 +- .../Internal/NpgsqlDataSourceManager.cs | 178 ++ .../Internal/NpgsqlRelationalConnection.cs | 11 +- .../Internal/NpgsqlTypeMappingSource.cs | 126 +- .../BuiltInDataTypesNpgsqlTest.cs | 18 +- .../EFCore.PG.FunctionalTests.csproj | 1 + .../JsonTypesNpgsqlTest.cs | 13 +- .../Query/EnumQueryTest.cs | 31 +- .../LegacyNpgsqlNodaTimeTypeMappingTest.cs | 105 + .../Query/LegacyTimestampQueryTest.cs | 2 +- .../Query/NodaTimeQueryNpgsqlTest.cs | 2028 +++++++++++++++++ .../Query/SpatialQueryNpgsqlFixture.cs | 11 +- .../Query/TimestampQueryTest.cs | 2 +- .../SpatialNpgsqlFixture.cs | 11 +- .../SpatialNpgsqlTest.cs | 5 + .../NpgsqlNorthwindTestStoreFactory.cs | 7 +- .../TestUtilities/NpgsqlTestStore.cs | 46 +- .../TestUtilities/NpgsqlTestStoreFactory.cs | 23 +- .../NodaTimeQueryNpgsqlTest.cs | 12 +- .../NpgsqlDbContextOptionsExtensionsTest.cs | 2 +- .../NpgsqlRelationalConnectionTest.cs | 90 +- .../Storage}/NpgsqlNodaTimeTypeMappingTest.cs | 3 +- .../Storage/NpgsqlTypeMappingSourceTest.cs | 2 +- .../Storage/NpgsqlTypeMappingTest.cs | 12 +- 57 files changed, 3386 insertions(+), 355 deletions(-) delete mode 100644 src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs create mode 100644 src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs create mode 100644 src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs rename src/EFCore.PG.NTS/Internal/{NpgsqlNetTopologySuiteOptions.cs => NpgsqlNetTopologySuiteSingletonOptions.cs} (54%) create mode 100644 src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs create mode 100644 src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs create mode 100644 src/EFCore.PG/Infrastructure/INpgsqlDataSourceConfigurationPlugin.cs create mode 100644 src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs create mode 100644 src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs create mode 100644 src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs rename test/{EFCore.PG.NodaTime.FunctionalTests => EFCore.PG.Tests/Storage}/NpgsqlNodaTimeTypeMappingTest.cs (99%) diff --git a/EFCore.PG.sln b/EFCore.PG.sln index dd965ca0d..9ccd15d74 100644 --- a/EFCore.PG.sln +++ b/EFCore.PG.sln @@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.PG.Tests", "test\EFC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.PG.FunctionalTests", "test\EFCore.PG.FunctionalTests\EFCore.PG.FunctionalTests.csproj", "{05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.PG.NodaTime.FunctionalTests", "test\EFCore.PG.NodaTime.FunctionalTests\EFCore.PG.NodaTime.FunctionalTests.csproj", "{B78A7825-BE72-4509-B0AD-01EEC67A9624}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.PG.NodaTime", "src\EFCore.PG.NodaTime\EFCore.PG.NodaTime.csproj", "{77F0608F-6D0C-481C-9108-D5176E2EAD69}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.NTS", "src\EFCore.PG.NTS\EFCore.PG.NTS.csproj", "{D7106D61-C7CA-4005-B31F-43281BB397AD}" @@ -70,18 +68,6 @@ Global {05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D}.Release|Any CPU.Build.0 = Release|Any CPU {05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D}.Release|x64.ActiveCfg = Release|Any CPU {05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D}.Release|x86.ActiveCfg = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|x64.ActiveCfg = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|x64.Build.0 = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|x86.ActiveCfg = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Debug|x86.Build.0 = Debug|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|Any CPU.Build.0 = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x64.ActiveCfg = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x64.Build.0 = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x86.ActiveCfg = Release|Any CPU - {B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x86.Build.0 = Release|Any CPU {77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|Any CPU.Build.0 = Debug|Any CPU {77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -114,7 +100,6 @@ Global {FADDA2D1-03B4-4DEF-8D24-DD1CA4E81F4A} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {E1D99AD4-D88B-42BA-86DF-90B98B2E9A01} = {ED612DB1-AB32-4603-95E7-891BACA71C39} {05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D} = {ED612DB1-AB32-4603-95E7-891BACA71C39} - {B78A7825-BE72-4509-B0AD-01EEC67A9624} = {ED612DB1-AB32-4603-95E7-891BACA71C39} {77F0608F-6D0C-481C-9108-D5176E2EAD69} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {D7106D61-C7CA-4005-B31F-43281BB397AD} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {26203F54-A3C1-43AD-A101-8F589D2D67AD} = {4A5A60DD-41B6-40BF-B677-227A921ECCC8} diff --git a/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs b/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs index f5550ba0a..8f3301540 100644 --- a/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs +++ b/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs @@ -25,5 +25,5 @@ public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollec => serviceCollection .AddSingleton() .AddSingleton() - .TryAddSingleton(); + .TryAddSingleton(); } diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs index a78ee485b..2374de24a 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs @@ -22,19 +22,26 @@ public static NpgsqlDbContextOptionsBuilder UseNetTopologySuite( Ordinates handleOrdinates = Ordinates.None, bool geographyAsDefault = false) { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite( - coordinateSequenceFactory, precisionModel, handleOrdinates, geographyAsDefault); -#pragma warning restore CS0618 - var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; var extension = coreOptionsBuilder.Options.FindExtension() ?? new NpgsqlNetTopologySuiteOptionsExtension(); + if (coordinateSequenceFactory is not null) + { + extension = extension.WithCoordinateSequenceFactory(coordinateSequenceFactory); + } + + if (precisionModel is not null) + { + extension = extension.WithPrecisionModel(precisionModel); + } + + if (handleOrdinates is not Ordinates.None) + { + extension = extension.WithHandleOrdinates(handleOrdinates); + } + if (geographyAsDefault) { extension = extension.WithGeographyDefault(); diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs index 779c0a0c6..c7a9b2708 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; @@ -21,15 +22,16 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite( { Check.NotNull(serviceCollection, nameof(serviceCollection)); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) - .TryAdd(p => p.GetRequiredService()) + new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd(p => p.GetRequiredService()) .TryAdd() .TryAdd() .TryAdd() .TryAdd() .TryAdd() .TryAddProviderSpecificServices( - x => x.TryAddSingleton()); + x => x.TryAddSingleton()); return serviceCollection; } diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs deleted file mode 100644 index 7c6e317cc..000000000 --- a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -/// -/// Represents options for Npgsql NetTopologySuite that can only be set at the singleton level. -/// -public interface INpgsqlNetTopologySuiteOptions : ISingletonOptions -{ - /// - /// True if geography is to be used by default instead of geometry - /// - bool IsGeographyDefault { get; } -} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs new file mode 100644 index 000000000..d8e5d7abc --- /dev/null +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs @@ -0,0 +1,41 @@ +// ReSharper disable once CheckNamespace + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +/// +/// Represents options for Npgsql NetTopologySuite that can only be set at the singleton level. +/// +public interface INpgsqlNetTopologySuiteSingletonOptions : ISingletonOptions +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + CoordinateSequenceFactory? CoordinateSequenceFactory { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + PrecisionModel? PrecisionModel { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + Ordinates HandleOrdinates { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + bool IsGeographyDefault { get; } +} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs new file mode 100644 index 000000000..d258c2889 --- /dev/null +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs @@ -0,0 +1,26 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class NetTopologySuiteDataSourceConfigurationPlugin(INpgsqlNetTopologySuiteSingletonOptions options) + : INpgsqlDataSourceConfigurationPlugin +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public void Configure(NpgsqlDataSourceBuilder npgsqlDataSourceBuilder) + => npgsqlDataSourceBuilder.UseNetTopologySuite( + options.CoordinateSequenceFactory, + options.PrecisionModel, + options.HandleOrdinates, + options.IsGeographyDefault); +} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs index 3d0205650..a217be224 100644 --- a/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs @@ -14,6 +14,30 @@ public class NpgsqlNetTopologySuiteOptionsExtension : IDbContextOptionsExtension { private DbContextOptionsExtensionInfo? _info; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual CoordinateSequenceFactory? CoordinateSequenceFactory { get; private set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual PrecisionModel? PrecisionModel { get; private set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Ordinates HandleOrdinates { get; private set; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -68,6 +92,52 @@ public virtual void ApplyServices(IServiceCollection services) public virtual DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlNetTopologySuiteOptionsExtension WithCoordinateSequenceFactory( + CoordinateSequenceFactory? coordinateSequenceFactory) + { + var clone = Clone(); + + clone.CoordinateSequenceFactory = coordinateSequenceFactory; + + return clone; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlNetTopologySuiteOptionsExtension WithPrecisionModel(PrecisionModel? precisionModel) + { + var clone = Clone(); + + clone.PrecisionModel = precisionModel; + + return clone; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlNetTopologySuiteOptionsExtension WithHandleOrdinates(Ordinates handleOrdinates) + { + var clone = Clone(); + + clone.HandleOrdinates = handleOrdinates; + + return clone; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -123,7 +193,11 @@ public override int GetServiceProviderHashCode() => Extension.IsGeographyDefault.GetHashCode(); public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) - => true; + => other is ExtensionInfo otherInfo + && ReferenceEquals(Extension.CoordinateSequenceFactory, otherInfo.Extension.CoordinateSequenceFactory) + && ReferenceEquals(Extension.PrecisionModel, otherInfo.Extension.PrecisionModel) + && Extension.HandleOrdinates == otherInfo.Extension.HandleOrdinates + && Extension.IsGeographyDefault == otherInfo.Extension.IsGeographyDefault; public override void PopulateDebugInfo(IDictionary debugInfo) { diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs similarity index 54% rename from src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs rename to src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs index cd559f8a0..f34dc4aa1 100644 --- a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs +++ b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs @@ -4,8 +4,17 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; /// -public class NpgsqlNetTopologySuiteOptions : INpgsqlNetTopologySuiteOptions +public class NpgsqlNetTopologySuiteSingletonOptions : INpgsqlNetTopologySuiteSingletonOptions { + /// + public virtual CoordinateSequenceFactory? CoordinateSequenceFactory { get; set; } + + /// + public virtual PrecisionModel? PrecisionModel { get; set; } + + /// + public virtual Ordinates HandleOrdinates { get; set; } + /// public virtual bool IsGeographyDefault { get; set; } @@ -15,6 +24,9 @@ public virtual void Initialize(IDbContextOptions options) var npgsqlNtsOptions = options.FindExtension() ?? new NpgsqlNetTopologySuiteOptionsExtension(); + CoordinateSequenceFactory = npgsqlNtsOptions.CoordinateSequenceFactory; + PrecisionModel = npgsqlNtsOptions.PrecisionModel; + HandleOrdinates = npgsqlNtsOptions.HandleOrdinates; IsGeographyDefault = npgsqlNtsOptions.IsGeographyDefault; } diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs index 786a1c1f4..2dcbd5a77 100644 --- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs +++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs @@ -15,7 +15,7 @@ public class NpgsqlNetTopologySuiteTypeMappingSourcePlugin : IRelationalTypeMapp { // Note: we reference the options rather than copying IsGeographyDefault out, because that field is initialized // rather late by SingletonOptionsInitializer - private readonly INpgsqlNetTopologySuiteOptions _options; + private readonly INpgsqlNetTopologySuiteSingletonOptions _options; private static bool TryGetClrType(string subtypeName, [NotNullWhen(true)] out Type? clrType) { @@ -41,7 +41,7 @@ private static bool TryGetClrType(string subtypeName, [NotNullWhen(true)] out Ty /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public NpgsqlNetTopologySuiteTypeMappingSourcePlugin(INpgsqlNetTopologySuiteOptions options) + public NpgsqlNetTopologySuiteTypeMappingSourcePlugin(INpgsqlNetTopologySuiteSingletonOptions options) { _options = Check.NotNull(options, nameof(options)); } diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs index 407291d94..3af53dc7c 100644 --- a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs @@ -18,11 +18,6 @@ public static NpgsqlDbContextOptionsBuilder UseNodaTime( { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); -#pragma warning restore CS0618 - var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; var extension = coreOptionsBuilder.Options.FindExtension() diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs index f070cc10c..377673736 100644 --- a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs +++ b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs @@ -1,3 +1,5 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -19,7 +21,8 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNodaTime( { Check.NotNull(serviceCollection, nameof(serviceCollection)); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) + new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs new file mode 100644 index 000000000..b6fb4e58d --- /dev/null +++ b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs @@ -0,0 +1,19 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class NodaTimeDataSourceConfigurationPlugin : INpgsqlDataSourceConfigurationPlugin +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public void Configure(NpgsqlDataSourceBuilder npgsqlDataSourceBuilder) + => npgsqlDataSourceBuilder.UseNodaTime(); +} diff --git a/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs b/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs index f3bb15a10..75fa2f835 100644 --- a/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs +++ b/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs @@ -2,7 +2,7 @@ [assembly: InternalsVisibleTo( - "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.FunctionalTests, PublicKey=" + "Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100" + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" diff --git a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs index daae6db8b..2cbdaa588 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs @@ -82,15 +82,40 @@ public override bool Create( switch (typeMapping) { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete case NpgsqlEnumTypeMapping enumTypeMapping: - if (enumTypeMapping.NameTranslator != NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator) + var code = Dependencies.CSharpHelper; + mainBuilder.AppendLine(";"); + + mainBuilder.AppendLine( + $"{parameters.TargetName}.TypeMapping = ((NpgsqlEnumTypeMapping){parameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + mainBuilder + .Append("unquotedStoreType: ") + .Append(code.Literal(enumTypeMapping.UnquotedStoreType)) + .AppendLine(",") + .AppendLine("labels: new Dictionary()") + .AppendLine("{") + .IncrementIndent(); + + foreach (var (enumValue, label) in enumTypeMapping.Labels) { - throw new NotSupportedException( - "Mapped enums are only supported in the compiled model if they use the default name translator"); + mainBuilder + .Append('[') + .Append(code.UnknownLiteral(enumValue)) + .Append(']') + .Append(" = ") + .Append(code.Literal(label)) + .AppendLine(","); } + + mainBuilder + .Append("}") + .DecrementIndent() + .Append(")") + .DecrementIndent(); + break; -#pragma warning restore CS0618 case NpgsqlRangeTypeMapping rangeTypeMapping: { @@ -127,7 +152,6 @@ public override bool Create( break; } - } return result; diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 6cfd490c1..f38fda9af 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs @@ -443,6 +443,61 @@ public static ModelBuilder HasPostgresEnum( return modelBuilder; } + /// + /// Registers a user-defined enum type in the model. + /// + /// The model builder in which to create the enum type. + /// The schema in which to create the enum type. + /// The name of the enum type to create. + /// The enum label values. + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html + /// + /// builder + public static IConventionModelBuilder HasPostgresEnum( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + string[] labels) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(labels, nameof(labels)); + + if (modelBuilder.CanSetPostgresEnum(schema, name)) + { + modelBuilder.Metadata.GetOrAddPostgresEnum(schema, name, labels); + } + return modelBuilder; + } + + /// + /// Returns a value indicating whether the given PostgreSQL extension can be registered in the model. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The model builder. + /// The schema in which to create the extension. + /// The name of the extension to create. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default increment for SQL Server IDENTITY. + public static bool CanSetPostgresEnum( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + bool fromDataAnnotation = false) + { + var annotationName = PostgresExtension.BuildAnnotationName(schema, name); + + return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name}", fromDataAnnotation); + } + /// /// Registers a user-defined enum type in the model. /// diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs index 018c77f5f..35228a506 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs @@ -321,6 +321,19 @@ public static PostgresEnum GetOrAddPostgresEnum( string[] labels) => PostgresEnum.GetOrAddPostgresEnum(model, schema, name, labels); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static PostgresEnum GetOrAddPostgresEnum( + this IConventionModel model, + string? schema, + string name, + string[] labels) + => PostgresEnum.GetOrAddPostgresEnum(model, schema, name, labels); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index 6335c3429..2cd339ca0 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -87,7 +87,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio { Check.NotNull(serviceCollection, nameof(serviceCollection)); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) + new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) .TryAdd() .TryAdd>() .TryAdd(p => p.GetRequiredService()) @@ -124,6 +124,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio .TryAddSingleton() .TryAddSingleton() .TryAddSingleton() + .TryAddSingleton() .TryAddScoped()) .TryAddCoreServices(); diff --git a/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs b/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs new file mode 100644 index 000000000..d6f25b764 --- /dev/null +++ b/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs @@ -0,0 +1,36 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +/// +/// A builder API designed for Npgsql when registering services. +/// +public class EntityFrameworkNpgsqlServicesBuilder : EntityFrameworkRelationalServicesBuilder +{ + private static readonly IDictionary NpgsqlServices + = new Dictionary + { + { + typeof(INpgsqlDataSourceConfigurationPlugin), + new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) + } + }; + + /// + /// Used by relational database providers to create a new for + /// registration of provider services. + /// + /// The collection to which services will be registered. + public EntityFrameworkNpgsqlServicesBuilder(IServiceCollection serviceCollection) + : base(serviceCollection) + { + } + + /// + /// Gets the for the given service type. + /// + /// The type that defines the service API. + /// The for the type or if it's not an EF service. + protected override ServiceCharacteristics? TryGetServiceCharacteristics(Type serviceType) + => NpgsqlServices.TryGetValue(serviceType, out var characteristics) + ? characteristics + : base.TryGetServiceCharacteristics(serviceType); +} diff --git a/src/EFCore.PG/Infrastructure/INpgsqlDataSourceConfigurationPlugin.cs b/src/EFCore.PG/Infrastructure/INpgsqlDataSourceConfigurationPlugin.cs new file mode 100644 index 000000000..6a76b1dee --- /dev/null +++ b/src/EFCore.PG/Infrastructure/INpgsqlDataSourceConfigurationPlugin.cs @@ -0,0 +1,24 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +/// +/// Represents a plugin that configures an via . +/// +/// +/// +/// The service lifetime is and multiple registrations +/// are allowed. This means a single instance of each service is used by many +/// instances. The implementation must be thread-safe. +/// This service cannot depend on services registered as . +/// +/// +/// See Implementation of database providers and extensions +/// for more information and examples. +/// +/// +public interface INpgsqlDataSourceConfigurationPlugin +{ + /// + /// Applies the plugin configuration on the given . + /// + void Configure(NpgsqlDataSourceBuilder npgsqlDataSourceBuilder); +} diff --git a/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs b/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs new file mode 100644 index 000000000..b34be200e --- /dev/null +++ b/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs @@ -0,0 +1,148 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public sealed class EnumDefinition : IEquatable +{ + /// + /// Maps the CLR member values to the PostgreSQL value labels. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public IReadOnlyDictionary Labels { get; } + + /// + /// The name of the PostgreSQL enum type to be mapped. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string StoreTypeName { get; } + + /// + /// The PostgreSQL schema in which the enum is defined. If null, the default schema is used + /// (which is public unless changed on the model). + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string? StoreTypeSchema { get; } + + /// + /// The CLR type of the enum. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public Type ClrType { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public INpgsqlNameTranslator NameTranslator { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public EnumDefinition( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type clrType, + string name, + string? schema, + INpgsqlNameTranslator nameTranslator) + { + if (clrType is not { IsEnum: true, IsClass: false }) + { + throw new ArgumentException($"Enum type mappings require a CLR enum. {clrType.FullName} is not an enum."); + } + + StoreTypeName = name; + StoreTypeSchema = schema; + ClrType = clrType; + + NameTranslator = nameTranslator; + Labels = clrType.GetFields(BindingFlags.Static | BindingFlags.Public) + .ToDictionary( + x => x.GetValue(null)!, + x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)); + } + + /// + public override bool Equals(object? obj) + => obj is EnumDefinition other && Equals(other); + + /// + public bool Equals(EnumDefinition? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(other, this)) + { + return true; + } + + if (StoreTypeName != other.StoreTypeName + || StoreTypeSchema != other.StoreTypeSchema + || ClrType != other.ClrType + || !ReferenceEquals(NameTranslator, other.NameTranslator) + || Labels.Count != other.Labels.Count) + { + return false; + } + + foreach (var (key, value) in Labels) + { + if (!other.Labels.TryGetValue(key, out var otherValue) + || value != otherValue) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(StoreTypeName); + hashCode.Add(StoreTypeSchema); + hashCode.Add(ClrType); + hashCode.Add(NameTranslator); + foreach (var (key, value) in Labels) + { + hashCode.Add(key); + hashCode.Add(value); + } + + return hashCode.ToHashCode(); + } +} diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs index 7618ae040..97910f632 100644 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs @@ -1,4 +1,5 @@ using System.Data.Common; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; @@ -28,17 +29,12 @@ public interface INpgsqlSingletonOptions : ISingletonOptions bool ReverseNullOrderingEnabled { get; } /// - /// The data source being used, or if a connection string or connection was provided directly. + /// The collection of enum mappings. /// - DbDataSource? DataSource { get; } + IReadOnlyList EnumDefinitions { get; } /// /// The collection of range mappings. /// IReadOnlyList UserRangeDefinitions { get; } - - /// - /// The root service provider for the application, if available. />. - /// - IServiceProvider? ApplicationServiceProvider { get; } } diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 1ed164bb4..60c5cb59a 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -2,6 +2,8 @@ using System.Globalization; using System.Net.Security; using System.Text; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; @@ -12,6 +14,7 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension { private DbContextOptionsExtensionInfo? _info; private readonly List _userRangeDefinitions; + private readonly List _enumDefinitions; private Version? _postgresVersion; @@ -57,6 +60,12 @@ public virtual bool IsPostgresVersionSet public virtual IReadOnlyList UserRangeDefinitions => _userRangeDefinitions; + /// + /// The list of range mappings specified by the user. + /// + public virtual IReadOnlyList EnumDefinitions + => _enumDefinitions; + /// /// The specified . /// @@ -85,6 +94,7 @@ public virtual IReadOnlyList UserRangeDefinitions public NpgsqlOptionsExtension() { _userRangeDefinitions = []; + _enumDefinitions = []; } // NB: When adding new options, make sure to update the copy ctor below. @@ -100,6 +110,7 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom) _postgresVersion = copyFrom._postgresVersion; UseRedshift = copyFrom.UseRedshift; _userRangeDefinitions = [..copyFrom._userRangeDefinitions]; + _enumDefinitions = [..copyFrom._enumDefinitions]; ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback; RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback; ProvidePasswordCallback = copyFrom.ProvidePasswordCallback; @@ -180,6 +191,31 @@ public virtual NpgsqlOptionsExtension WithUserRangeDefinition( return clone; } + /// + /// Returns a copy of the current instance configured with the specified range mapping. + /// + public virtual NpgsqlOptionsExtension WithEnumMapping( + Type clrType, + string enumName, + string? schemaName, + INpgsqlNameTranslator? nameTranslator) + { + if (clrType is not { IsEnum: true, IsClass: false}) + { + throw new ArgumentException($"Enum type mappings require a CLR enum. {clrType.FullName} is not an enum."); + } + + var clone = (NpgsqlOptionsExtension)Clone(); + +#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete + nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; +#pragma warning restore CS0618 + + clone._enumDefinitions.Add(new EnumDefinition(clrType, enumName, schemaName, nameTranslator)); + + return clone; + } + /// /// Returns a copy of the current instance configured to use the specified administrative database. /// @@ -397,12 +433,12 @@ public override string LogFragment { builder.Append(item.SubtypeClrType).Append("=>"); - if (item.SchemaName is not null) + if (item.StoreTypeSchema is not null) { - builder.Append(item.SchemaName).Append("."); + builder.Append(item.StoreTypeSchema).Append("."); } - builder.Append(item.RangeName); + builder.Append(item.StoreTypeName); if (item.SubtypeName is not null) { @@ -453,8 +489,8 @@ public override int GetServiceProviderHashCode() public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => other is ExtensionInfo otherInfo && Extension.PostgresVersion == otherInfo.Extension.PostgresVersion - && ReferenceEquals(Extension.DataSource, otherInfo.Extension.DataSource) && Extension.ReverseNullOrdering == otherInfo.Extension.ReverseNullOrdering + && Extension.EnumDefinitions.SequenceEqual(otherInfo.Extension.EnumDefinitions) && Extension.UserRangeDefinitions.SequenceEqual(otherInfo.Extension.UserRangeDefinitions) && Extension.UseRedshift == otherInfo.Extension.UseRedshift; @@ -482,6 +518,16 @@ public override void PopulateDebugInfo(IDictionary debugInfo) debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.ProvidePasswordCallback)] = (Extension.ProvidePasswordCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); + foreach (var enumDefinition in Extension._enumDefinitions) + { + debugInfo[ + "Npgsql.EntityFrameworkCore.PostgreSQL:" + + nameof(NpgsqlDbContextOptionsBuilder.MapEnum) + + ":" + + enumDefinition.ClrType.Name] + = enumDefinition.GetHashCode().ToString(CultureInfo.InvariantCulture); + } + foreach (var rangeDefinition in Extension._userRangeDefinitions) { debugInfo[ @@ -496,50 +542,3 @@ public override void PopulateDebugInfo(IDictionary debugInfo) #endregion Infrastructure } - -/// -/// A definition for a user-defined PostgreSQL range to be mapped. -/// -public record UserRangeDefinition -{ - /// - /// The name of the PostgreSQL range type to be mapped. - /// - public virtual string RangeName { get; } - - /// - /// The PostgreSQL schema in which the range is defined. If null, the default schema is used - /// (which is public unless changed on the model). - /// - public virtual string? SchemaName { get; } - - /// - /// The CLR type of the range's subtype (or element). - /// The actual mapped type will be an over this type. - /// - public virtual Type SubtypeClrType { get; } - - /// - /// Optionally, the name of the range's PostgreSQL subtype (or element). - /// This is usually not needed - the subtype will be inferred based on . - /// - public virtual string? SubtypeName { get; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public UserRangeDefinition( - string rangeName, - string? schemaName, - Type subtypeClrType, - string? subtypeName) - { - RangeName = Check.NotEmpty(rangeName, nameof(rangeName)); - SchemaName = schemaName; - SubtypeClrType = Check.NotNull(subtypeClrType, nameof(subtypeClrType)); - SubtypeName = subtypeName; - } -} diff --git a/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs b/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs new file mode 100644 index 000000000..8c5908a28 --- /dev/null +++ b/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs @@ -0,0 +1,74 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +/// +/// A definition for a user-defined PostgreSQL range to be mapped. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public sealed record UserRangeDefinition +{ + /// + /// The name of the PostgreSQL range type to be mapped. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string StoreTypeName { get; } + + /// + /// The PostgreSQL schema in which the range is defined. If null, the default schema is used + /// (which is public unless changed on the model). + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string? StoreTypeSchema { get; } + + /// + /// The CLR type of the range's subtype (or element). + /// The actual mapped type will be an over this type. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public Type SubtypeClrType { get; } + + /// + /// Optionally, the name of the range's PostgreSQL subtype (or element). + /// This is usually not needed - the subtype will be inferred based on . + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string? SubtypeName { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public UserRangeDefinition(string name, string? schema, Type subtypeClrType, string? subtypeName) + { + StoreTypeName = name; + StoreTypeSchema = schema; + SubtypeClrType = subtypeClrType; + SubtypeName = subtypeName; + } +} diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs index 4a43c55b0..b0a8e9921 100644 --- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs +++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs @@ -95,6 +95,33 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange( string? subtypeName = null) => WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName)); + /// + /// Maps a PostgreSQL enum type for use. + /// + /// The name of the PostgreSQL enum type to be mapped. + /// The name of the PostgreSQL schema in which the range is defined. + /// The name translator used to map enum value names to PostgreSQL enum values. + public virtual NpgsqlDbContextOptionsBuilder MapEnum( + string enumName, + string? schemaName = null, + INpgsqlNameTranslator? nameTranslator = null) + where T : struct, Enum + => MapEnum(typeof(T), enumName, schemaName, nameTranslator); + + /// + /// Maps a PostgreSQL enum type for use. + /// + /// The CLR type of the enum. + /// The name of the PostgreSQL enum type to be mapped. + /// The name of the PostgreSQL schema in which the range is defined. + /// The name translator used to map enum value names to PostgreSQL enum values. + public virtual NpgsqlDbContextOptionsBuilder MapEnum( + Type clrType, + string enumName, + string? schemaName = null, + INpgsqlNameTranslator? nameTranslator = null) + => WithOption(e => e.WithEnumMapping(clrType, enumName, schemaName, nameTranslator)); + /// /// Appends NULLS FIRST to all ORDER BY clauses. This is important for the tests which were written /// for SQL Server. Note that to fully implement null-first ordering indexes also need to be generated diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs index e0fa14260..46c9f9a7c 100644 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs @@ -1,6 +1,7 @@ using System.Data.Common; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; @@ -45,7 +46,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual DbDataSource? DataSource { get; private set; } + public IReadOnlyList EnumDefinitions { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -55,14 +56,6 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public virtual IReadOnlyList UserRangeDefinitions { get; private set; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IServiceProvider? ApplicationServiceProvider { get; private set; } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -71,6 +64,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public NpgsqlSingletonOptions() { + EnumDefinitions = []; UserRangeDefinitions = []; } @@ -78,20 +72,13 @@ public NpgsqlSingletonOptions() public virtual void Initialize(IDbContextOptions options) { var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension(); - var coreOptions = options.FindExtension() ?? new CoreOptionsExtension(); PostgresVersion = npgsqlOptions.PostgresVersion; IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet; UseRedshift = npgsqlOptions.UseRedshift; ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; + EnumDefinitions = npgsqlOptions.EnumDefinitions; UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions; - - // TODO: Remove after https://github.com/dotnet/efcore/pull/29950 - ApplicationServiceProvider = coreOptions.ApplicationServiceProvider; - - DataSource = npgsqlOptions.DataSource ?? (npgsqlOptions.ConnectionString is null && npgsqlOptions.Connection is null - ? coreOptions.ApplicationServiceProvider?.GetService() - : null); } /// @@ -123,10 +110,12 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } - if (npgsqlOptions.DataSource is not null && !ReferenceEquals(DataSource, npgsqlOptions.DataSource)) + if (!EnumDefinitions.SequenceEqual(npgsqlOptions.EnumDefinitions)) { throw new InvalidOperationException( - NpgsqlStrings.TwoDataSourcesInSameServiceProvider(nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + CoreStrings.SingletonOptionChanged( + nameof(NpgsqlDbContextOptionsBuilder.MapEnum), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions)) diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs index 8b9098fbd..6b296e878 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs @@ -1,4 +1,5 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; @@ -19,6 +20,7 @@ public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder { private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly Version _postgresVersion; + private readonly IReadOnlyList _enumDefinitions; /// /// Creates a new instance. @@ -36,6 +38,7 @@ public NpgsqlConventionSetBuilder( { _typeMappingSource = typeMappingSource; _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + _enumDefinitions = npgsqlSingletonOptions.EnumDefinitions; } /// @@ -70,7 +73,7 @@ public override ConventionSet CreateConventionSet() conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention); - conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresModelFinalizingConvention(_typeMappingSource)); + conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresModelFinalizingConvention(_typeMappingSource, _enumDefinitions)); ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention); ReplaceConvention( conventionSet.ModelFinalizingConventions, diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs index b7ed9e0ae..6298c6dca 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs @@ -1,3 +1,5 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; /// @@ -9,14 +11,19 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; public class NpgsqlPostgresModelFinalizingConvention : IModelFinalizingConvention { private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly IReadOnlyList _enumDefinitions; /// /// Creates a new instance of . /// /// The type mapping source to use. - public NpgsqlPostgresModelFinalizingConvention(IRelationalTypeMappingSource typeMappingSource) + /// + public NpgsqlPostgresModelFinalizingConvention( + IRelationalTypeMappingSource typeMappingSource, + IReadOnlyList enumDefinitions) { _typeMappingSource = typeMappingSource; + _enumDefinitions = enumDefinitions; } /// @@ -36,6 +43,22 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, } } } + + SetupEnums(modelBuilder); + } + + /// + /// Configures the model to create PostgreSQL enums based on the user's enum definitions in the context options. + /// + protected virtual void SetupEnums(IConventionModelBuilder modelBuilder) + { + foreach (var enumDefinition in _enumDefinitions) + { + modelBuilder.HasPostgresEnum( + enumDefinition.StoreTypeSchema, + enumDefinition.StoreTypeName, + enumDefinition.Labels.Values.Order(StringComparer.Ordinal).ToArray()); + } } /// @@ -46,6 +69,7 @@ protected virtual void DiscoverPostgresExtensions( RelationalTypeMapping typeMapping, IConventionModelBuilder modelBuilder) { + // TODO: does not work if CREATE EXTENSION was done on a non-default schema. #3177 switch (typeMapping.StoreType) { case "hstore": diff --git a/src/EFCore.PG/Metadata/PostgresEnum.cs b/src/EFCore.PG/Metadata/PostgresEnum.cs index f31cd651a..0d373c0cc 100644 --- a/src/EFCore.PG/Metadata/PostgresEnum.cs +++ b/src/EFCore.PG/Metadata/PostgresEnum.cs @@ -70,6 +70,49 @@ public static PostgresEnum GetOrAddPostgresEnum( return new PostgresEnum(annotatable, annotationName) { Labels = labels }; } + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the enum. + /// The enum schema or null to use the model's default schema. + /// The enum name. + /// The enum labels. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static PostgresEnum GetOrAddPostgresEnum( + IConventionAnnotatable annotatable, + string? schema, + string name, + string[] labels) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(labels, nameof(labels)); + + if (FindPostgresEnum(annotatable, schema, name) is { } enumType) + { + return enumType; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new PostgresEnum(annotatable, annotationName) { Labels = labels }; + } + /// /// Gets or adds a from or to the . /// diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 03fc496de..f2f1e6405 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -1262,9 +1262,9 @@ protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, GenerateDropEnum(enumTypeToDrop, model, builder); } - foreach (var (newEnum, oldEnum) in operation.GetPostgresEnums() + foreach (var (newEnum, oldEnum) in operation.GetPostgresEnums().OrderBy(e => e.Schema).ThenBy(e => e.Name) .Join( - operation.GetOldPostgresEnums(), + operation.GetOldPostgresEnums().OrderBy(e => e.Schema).ThenBy(e => e.Name), e => new { e.Name, e.Schema }, e => new { e.Name, e.Schema }, (ne, oe) => (New: ne, Old: oe))) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs index 4cf7a59de..27a02bb17 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs @@ -438,7 +438,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I new[] { arguments[1], arguments[2] }, nullable: true, argumentsPropagateNullability: new[] { true, true }, - typeof(DateOnly?), + typeof(DateOnly), _typeMappingSource.FindMapping(typeof(DateOnly)) ); } @@ -450,7 +450,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I new[] { arguments[1], arguments[2] }, nullable: true, argumentsPropagateNullability: new[] { true, true }, - typeof(DateTime?), + typeof(DateTime), _typeMappingSource.FindMapping(typeof(DateTime)) ); } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs index b986d9122..ef0e1aa0d 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs @@ -234,7 +234,25 @@ public override DbParameter CreateParameter( } } - return base.CreateParameter(command, name, value, nullable, direction); + var param = base.CreateParameter(command, name, value, nullable, direction); + if (param is not NpgsqlParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {param.GetType().Name}"); + } + + // Enums and user-defined ranges require setting NpgsqlParameter.DataTypeName to specify the PostgreSQL type name. + // Make this work for arrays over these types as well. + switch (ElementTypeMapping) + { + case NpgsqlEnumTypeMapping enumTypeMapping: + npgsqlParameter.DataTypeName = enumTypeMapping.UnquotedStoreType + "[]"; + break; + case NpgsqlRangeTypeMapping { UnquotedStoreType: string unquotedStoreType }: + npgsqlParameter.DataTypeName = unquotedStoreType + "[]"; + break; + } + return param; } /// diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs index 94f1ead19..faf8cc37b 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Data.Common; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -12,17 +13,26 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; public class NpgsqlEnumTypeMapping : RelationalTypeMapping { /// - /// Translates the CLR member value to the PostgreSQL value label. + /// Maps the CLR member values to the PostgreSQL value labels. /// - private readonly Dictionary _members; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyDictionary Labels { get; } /// + /// The unquoted store type, used for setting on . + /// + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static NpgsqlEnumTypeMapping Default { get; } = new(); + /// + public virtual string UnquotedStoreType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -30,7 +40,7 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual INpgsqlNameTranslator NameTranslator { get; } + public static NpgsqlEnumTypeMapping Default { get; } = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -38,9 +48,13 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTranslator? nameTranslator = null) + public NpgsqlEnumTypeMapping( + string quotedStoreType, + string unquotedStoreType, + Type enumType, + IReadOnlyDictionary labels) : base( - storeType, + quotedStoreType, enumType, jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance( typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType))) @@ -50,12 +64,8 @@ public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTransla throw new ArgumentException($"Enum type mappings require a CLR enum. {enumType.FullName} is not an enum."); } -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; -#pragma warning restore CS0618 - - NameTranslator = nameTranslator; - _members = CreateValueMapping(enumType, nameTranslator); + UnquotedStoreType = unquotedStoreType; + Labels = labels; } /// @@ -66,11 +76,12 @@ public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTransla /// protected NpgsqlEnumTypeMapping( RelationalTypeMappingParameters parameters, - INpgsqlNameTranslator nameTranslator) + string unquotedStoreType, + IReadOnlyDictionary labels) : base(parameters) { - NameTranslator = nameTranslator; - _members = CreateValueMapping(parameters.CoreParameters.ClrType, nameTranslator); + UnquotedStoreType = unquotedStoreType; + Labels = labels; } // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled @@ -78,10 +89,8 @@ protected NpgsqlEnumTypeMapping( private NpgsqlEnumTypeMapping() : base("some_enum", typeof(int)) { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - NameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; -#pragma warning restore CS0618 - _members = null!; + UnquotedStoreType = "some_enum"; + Labels = new Dictionary(); } /// @@ -91,7 +100,19 @@ private NpgsqlEnumTypeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlEnumTypeMapping(parameters, NameTranslator); + => new NpgsqlEnumTypeMapping(parameters, UnquotedStoreType, Labels); + + /// + /// This method exists only to support the compiled model. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual NpgsqlEnumTypeMapping Clone(string unquotedStoreType, IReadOnlyDictionary labels) + => new(Parameters, unquotedStoreType, labels); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -99,8 +120,16 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override string GenerateNonNullSqlLiteral(object value) - => $"'{_members[value]}'::{StoreType}"; + protected override void ConfigureParameter(DbParameter parameter) + { + if (parameter is not NpgsqlParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {parameter.GetType().Name}"); + } + + npgsqlParameter.DataTypeName = UnquotedStoreType; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -108,11 +137,8 @@ protected override string GenerateNonNullSqlLiteral(object value) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - private static Dictionary CreateValueMapping(Type enumType, INpgsqlNameTranslator nameTranslator) - => enumType.GetFields(BindingFlags.Static | BindingFlags.Public) - .ToDictionary( - x => x.GetValue(null)!, - x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)); + protected override string GenerateNonNullSqlLiteral(object value) + => $"'{Labels[value]}'::{StoreType}"; // This is public for the compiled model /// diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs index d8e5d62ae..e738b8170 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs @@ -42,7 +42,7 @@ public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping /// For user-defined ranges, we have no and so the PG type name is set on /// instead. /// - private string? PgDataTypeName { get; init; } + public virtual string? UnquotedStoreType { get; init; } /// /// Constructs an instance of the class for a built-in range type which has a @@ -74,7 +74,7 @@ public static NpgsqlRangeTypeMapping CreatUserDefinedRangeMapping( RelationalTypeMapping subtypeMapping) => new(quotedRangeStoreType, rangeClrType, rangeNpgsqlDbType: NpgsqlDbType.Unknown, subtypeMapping) { - PgDataTypeName = unquotedRangeStoreType + UnquotedStoreType = unquotedRangeStoreType }; private NpgsqlRangeTypeMapping( @@ -139,7 +139,7 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p protected override void ConfigureParameter(DbParameter parameter) { // Built-in range types have an NpgsqlDbType, so we just do the normal thing. - if (PgDataTypeName is null) + if (UnquotedStoreType is null) { Check.DebugAssert(NpgsqlDbType is not NpgsqlDbType.Unknown, "NpgsqlDbType is Unknown but no PgDataTypeName is configured"); base.ConfigureParameter(parameter); @@ -154,7 +154,7 @@ protected override void ConfigureParameter(DbParameter parameter) $"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {parameter.GetType().Name}"); } - npgsqlParameter.DataTypeName = PgDataTypeName; + npgsqlParameter.DataTypeName = UnquotedStoreType; } /// diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs new file mode 100644 index 000000000..9859e2cd0 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs @@ -0,0 +1,178 @@ +using System.Collections.Concurrent; +using System.Data.Common; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +/// +/// Manages resolving and creating instances. +/// +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +/// +/// The service lifetime is . This means a single instance +/// is used by many instances. The implementation must be thread-safe. +/// This service cannot depend on services registered as . +/// +/// +/// See Implementation of database providers and extensions +/// for more information and examples. +/// +/// +public class NpgsqlDataSourceManager : IDisposable, IAsyncDisposable +{ + private readonly IEnumerable _plugins; + private readonly ConcurrentDictionary _dataSources = new(); + private volatile int _isDisposed; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public NpgsqlDataSourceManager(IEnumerable plugins) + => _plugins = plugins.ToArray(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual DbDataSource? GetDataSource(NpgsqlOptionsExtension? npgsqlOptionsExtension, IServiceProvider? applicationServiceProvider) + => npgsqlOptionsExtension switch + { + // If the user has explicitly passed in a data source via UseNpgsql(), use that. + // Note that in this case, the data source is scoped (not singleton), and so can change between different + // DbContext instances using the same internal service provider. + { DataSource: DbDataSource dataSource } => dataSource, + + // If the user has passed in a DbConnection, never use a data source - even if e.g. MapEnum() was called. + // This is to avoid blocking and allow continuing using enums in conjunction with DbConnections (which + // must be manually set up by the user for the enum, of course). + { Connection: not null } => null, + + // If the user hasn't configured anything in UseNpgsql (no data source, no connection, no connection string), check the + // application service provider to see if a data source is registered there, and return that. + { ConnectionString: null } when applicationServiceProvider?.GetService() is DbDataSource dataSource + => dataSource, + + // Otherwise if there's no connection string, abort: a connection string is required to create a data source in any case. + { ConnectionString: null } or null => null, + + // The following are features which require an NpgsqlDataSource, since they require configuration on NpgsqlDataSourceBuilder. + { EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension), + _ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension), + + // If there's no configured feature which requires us to use a data source internally, don't use one; this causes + // NpgsqlRelationalConnection to use the connection string as before (no data source), allowing switching connection strings + // with the same service provider etc. + _ => null + }; + + private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) + { + var connectionString = npgsqlOptionsExtension.ConnectionString; + Check.DebugAssert(connectionString is not null, "Connection string can't be null"); + + if (_dataSources.TryGetValue(connectionString, out var dataSource)) + { + return dataSource; + } + + var newDataSource = CreateDataSource(npgsqlOptionsExtension); + + var addedDataSource = _dataSources.GetOrAdd(connectionString, newDataSource); + if (!ReferenceEquals(addedDataSource, newDataSource)) + { + newDataSource.Dispose(); + } + else if (_isDisposed == 1) + { + newDataSource.Dispose(); + throw new ObjectDisposedException(nameof(NpgsqlDataSourceManager)); + } + + return addedDataSource; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual NpgsqlDataSource CreateDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) + { + var dataSourceBuilder = new NpgsqlDataSourceBuilder(npgsqlOptionsExtension.ConnectionString); + + foreach (var enumDefinition in npgsqlOptionsExtension.EnumDefinitions) + { + dataSourceBuilder.MapEnum( + enumDefinition.ClrType, + enumDefinition.StoreTypeSchema is null + ? enumDefinition.StoreTypeName + : enumDefinition.StoreTypeSchema + "." + enumDefinition.StoreTypeName, + enumDefinition.NameTranslator); + } + + foreach (var plugin in _plugins) + { + plugin.Configure(dataSourceBuilder); + } + + // Legacy authentication-related callbacks at the EF level; apply these when building a data source as well. + if (npgsqlOptionsExtension.ProvideClientCertificatesCallback is not null) + { + dataSourceBuilder.UseClientCertificatesCallback(x => npgsqlOptionsExtension.ProvideClientCertificatesCallback(x)); + } + + if (npgsqlOptionsExtension.RemoteCertificateValidationCallback is not null) + { + dataSourceBuilder.UseUserCertificateValidationCallback(npgsqlOptionsExtension.RemoteCertificateValidationCallback); + } + + return dataSourceBuilder.Build(); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public void Dispose() + { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + { + foreach (var dataSource in _dataSources.Values) + { + dataSource.Dispose(); + } + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + { + foreach (var dataSource in _dataSources.Values) + { + await dataSource.DisposeAsync().ConfigureAwait(false); + } + } + } +} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs index 77b3f81e2..d4363b4d7 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs @@ -35,8 +35,15 @@ protected override bool SupportsAmbientTransactions /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public NpgsqlRelationalConnection(RelationalConnectionDependencies dependencies, INpgsqlSingletonOptions options) - : this(dependencies, options.DataSource) + public NpgsqlRelationalConnection( + RelationalConnectionDependencies dependencies, + NpgsqlDataSourceManager dataSourceManager, + IDbContextOptions options) + : this( + dependencies, + dataSourceManager.GetDataSource( + options.FindExtension(), + options.FindExtension()?.ApplicationServiceProvider)) { } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 5c03e6868..cff84bdb2 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -57,6 +57,7 @@ static NpgsqlTypeMappingSource() /// protected virtual ConcurrentDictionary ClrTypeMappings { get; } + private readonly IReadOnlyList _enumDefinitions; private readonly IReadOnlyList _userRangeDefinitions; private readonly bool _supportsMultiranges; @@ -348,68 +349,10 @@ public NpgsqlTypeMappingSource( StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - LoadUserDefinedTypeMappings(sqlGenerationHelper, options.DataSource as NpgsqlDataSource); - + _enumDefinitions = options.EnumDefinitions; _userRangeDefinitions = options.UserRangeDefinitions; } - /// - /// To be used in case user-defined mappings are added late, after this TypeMappingSource has already been initialized. - /// This is basically only for test usage. - /// - public virtual void LoadUserDefinedTypeMappings( - ISqlGenerationHelper sqlGenerationHelper, - NpgsqlDataSource? dataSource) - => SetupEnumMappings(sqlGenerationHelper, dataSource); - -#pragma warning disable NPG9001 - /// - /// Gets all global enum mappings from the ADO.NET layer and creates mappings for them - /// - protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelper, NpgsqlDataSource? dataSource) - { - List? adoEnumMappings = null; - - if (dataSource is not null - && typeof(NpgsqlDataSource).GetField("_hackyEnumTypeMappings", BindingFlags.NonPublic | BindingFlags.Instance) is - { } dataSourceTypeMappingsFieldInfo - && dataSourceTypeMappingsFieldInfo.GetValue(dataSource) is List dataSourceEnumMappings) - { - // Note that the data source's enum mappings also include any global ones that were configured when the data source was created. - // So we don't need to also collect mappings from GlobalTypeMapper below. - adoEnumMappings = dataSourceEnumMappings; - } -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - else if (NpgsqlConnection.GlobalTypeMapper.GetType().GetProperty( - "HackyEnumTypeMappings", BindingFlags.NonPublic | BindingFlags.Instance) - is PropertyInfo globalEnumTypeMappingsProperty - && globalEnumTypeMappingsProperty.GetValue(NpgsqlConnection.GlobalTypeMapper) is List - globalEnumMappings) - { - adoEnumMappings = globalEnumMappings; - } -#pragma warning restore CS0618 - - if (adoEnumMappings is not null) - { - foreach (var adoEnumMapping in adoEnumMappings) - { - // TODO: update with schema per https://github.com/npgsql/npgsql/issues/2121 - var components = adoEnumMapping.PgTypeName.Split('.'); - var schema = components.Length > 1 ? components.First() : null; - var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : adoEnumMapping.PgTypeName; - - var mapping = new NpgsqlEnumTypeMapping( - sqlGenerationHelper.DelimitIdentifier(name, schema), - adoEnumMapping.EnumClrType, - adoEnumMapping.NameTranslator); - ClrTypeMappings[adoEnumMapping.EnumClrType] = mapping; - StoreTypeMappings[mapping.StoreType] = [mapping]; - } - } - } -#pragma warning restore NPG9001 - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -420,6 +363,7 @@ is PropertyInfo globalEnumTypeMappingsProperty // First, try any plugins, allowing them to override built-in mappings (e.g. NodaTime) => base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo) + ?? FindEnumMapping(mappingInfo) ?? FindRowValueMapping(mappingInfo)?.Clone(mappingInfo) ?? FindUserRangeMapping(mappingInfo); @@ -816,6 +760,58 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) ? new NpgsqlRowValueTypeMapping(clrType) : null; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual RelationalTypeMapping? FindEnumMapping(in RelationalTypeMappingInfo mappingInfo) + { + var storeType = mappingInfo.StoreTypeName; + var clrType = mappingInfo.ClrType; + + if (clrType is not null and not { IsEnum: true, IsClass: false }) + { + return null; + } + + // Try to find an enum definition (defined by the user on their context options), based on the + // incoming MappingInfo's StoreType or ClrType + EnumDefinition? enumDefinition; + if (storeType is null) + { + enumDefinition = _enumDefinitions.SingleOrDefault(m => m.ClrType == clrType); + } + else + { + // TODO: Not sure what to do about quoting. Is the user expected to configure properties + // TODO: with a quoted (schema-qualified) store type or not? + var dot = storeType.IndexOf('.'); + enumDefinition = dot is -1 + ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType) + : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == storeType[(dot + 1)..] && m.StoreTypeSchema == storeType[..dot]); + } + + if (enumDefinition is null) + { + return null; + } + + // We now have an enum definition from the context options. + + // We need the following store type names: + // 1. The quoted type name is used in migrations, where quoting is needed + // 2. The unquoted type name is set on NpgsqlParameter.DataTypeName + // (though see https://github.com/npgsql/npgsql/issues/5710). + var (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); + return new NpgsqlEnumTypeMapping( + _sqlGenerationHelper.DelimitIdentifier(name, schema), + schema is null ? name : schema + "." + name, + enumDefinition.ClrType, + enumDefinition.Labels); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -838,7 +834,7 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) // incoming MappingInfo's StoreType or ClrType if (rangeStoreType is not null) { - rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.RangeName == rangeStoreType); + rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.StoreTypeName == rangeStoreType); if (rangeDefinition is null) { @@ -875,16 +871,16 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) if (subtypeMapping is null) { - throw new Exception($"Could not map range {rangeDefinition.RangeName}, no mapping was found its subtype"); + throw new Exception($"Could not map range {rangeDefinition.StoreTypeName}, no mapping was found its subtype"); } // We need to store types for the user-defined range: // 1. The quoted type name is used in migrations, where quoting is needed // 2. The unquoted type name is set on NpgsqlParameter.DataTypeName - var quotedRangeStoreType = _sqlGenerationHelper.DelimitIdentifier(rangeDefinition.RangeName, rangeDefinition.SchemaName); - var unquotedRangeStoreType = rangeDefinition.SchemaName is null - ? rangeDefinition.RangeName - : rangeDefinition.SchemaName + '.' + rangeDefinition.RangeName; + var quotedRangeStoreType = _sqlGenerationHelper.DelimitIdentifier(rangeDefinition.StoreTypeName, rangeDefinition.StoreTypeSchema); + var unquotedRangeStoreType = rangeDefinition.StoreTypeSchema is null + ? rangeDefinition.StoreTypeName + : rangeDefinition.StoreTypeSchema + '.' + rangeDefinition.StoreTypeName; return NpgsqlRangeTypeMapping.CreatUserDefinedRangeMapping( quotedRangeStoreType, unquotedRangeStoreType, rangeClrType, subtypeMapping); diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs index 4d372c8e1..69125d02e 100644 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs @@ -962,8 +962,10 @@ public override bool SupportsDecimalComparisons public override bool PreservesDateTimeKind => false; + // We instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's MapEnum() to function properly and instantiate an NpgsqlDataSource internally. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => new NpgsqlTestStoreFactory(useConnectionString: true); protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Query.Name; @@ -971,23 +973,13 @@ protected override bool ShouldLogCategory(string logCategory) public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - static BuiltInDataTypesNpgsqlFixture() - { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - NpgsqlConnection.GlobalTypeMapper.MapEnum(); -#pragma warning restore CS0618 - } + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseNpgsql(o => o.MapEnum("mood")); protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); - // TODO: Switch to using data source - ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings( - context.GetService(), dataSource: null); - - modelBuilder.HasPostgresEnum("mood", ["happy", "sad"]); - MakeRequired(modelBuilder); // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index 421d004c8..ce5bd43a8 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs index c02e580e1..593c41c26 100644 --- a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs @@ -461,16 +461,11 @@ protected override IServiceCollection AddServices(IServiceCollection serviceColl protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { - new NpgsqlDbContextOptionsBuilder(builder).UseNetTopologySuite(); - return builder; - } - - static JsonTypesNpgsqlTest() - { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete // Note that the enum doesn't actually need to be created in the database, since Can_read_and_write_JSON_value doesn't access // the database. We just need the mapping to be picked up by EFCore.PG from the ADO.NET layer. - NpgsqlConnection.GlobalTypeMapper.MapEnum("test.mapped_enum"); -#pragma warning restore CS0618 + new NpgsqlDbContextOptionsBuilder(builder) + .MapEnum("mapped_enum", "test") + .UseNetTopologySuite(); + return builder; } } diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index 479506646..f73103648 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -1,4 +1,5 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; @@ -223,12 +224,7 @@ public class EnumContext(DbContextOptions options) : PoolableDbContext(options) public DbSet SomeEntities { get; set; } protected override void OnModelCreating(ModelBuilder builder) - => builder - .HasPostgresEnum("mapped_enum", ["happy", "sad"]) - .HasPostgresEnum() - .HasPostgresEnum() - .HasDefaultSchema("test") - .HasPostgresEnum(); + => builder.HasDefaultSchema("test"); public static void Seed(EnumContext context) { @@ -292,20 +288,25 @@ public class EnumFixture : SharedStoreFixtureBase, IQueryFixtureBas protected override string StoreName => "EnumQueryTest"; + // We instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => new NpgsqlTestStoreFactory(useConnectionString: true); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - static EnumFixture() + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - NpgsqlConnection.GlobalTypeMapper.MapEnum("test.mapped_enum"); - NpgsqlConnection.GlobalTypeMapper.MapEnum("test.inferred_enum"); - NpgsqlConnection.GlobalTypeMapper.MapEnum("test.byte_enum"); - NpgsqlConnection.GlobalTypeMapper.MapEnum("test.schema_qualified_enum"); -#pragma warning restore CS0618 + var optionsBuilder = base.AddOptions(builder); + + new NpgsqlDbContextOptionsBuilder(optionsBuilder) + .MapEnum("mapped_enum", "test") + .MapEnum("inferred_enum", "test") + .MapEnum("byte_enum", "test") + .MapEnum("schema_qualified_enum", "test"); + + return optionsBuilder; } private EnumData _expectedData; diff --git a/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs new file mode 100644 index 000000000..1d1d41641 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs @@ -0,0 +1,105 @@ +using Microsoft.EntityFrameworkCore.Storage.Json; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +#if DEBUG + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + [Collection("LegacyNodaTimeTest")] + public class LegacyNpgsqlNodaTimeTypeMappingTest + : IClassFixture + { + [Fact] + public void Timestamp_maps_to_Instant_by_default() + => Assert.Same(typeof(Instant), GetMapping("timestamp without time zone").ClrType); + + [Fact] + public void Timestamptz_maps_to_Instant_by_default() + => Assert.Same(typeof(Instant), GetMapping("timestamp with time zone").ClrType); + + [Fact] + public void LocalDateTime_does_not_map_to_timestamptz() + => Assert.Null(GetMapping(typeof(LocalDateTime), "timestamp with time zone")); + + [Fact] + public void GenerateSqlLiteral_returns_instant_literal() + { + var mapping = GetMapping(typeof(Instant)); + Assert.Equal("timestamp without time zone", mapping.StoreType); + + var instant = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)).InUtc().ToInstant(); + Assert.Equal("TIMESTAMP '2018-04-20T10:31:33.666666Z'", mapping.GenerateSqlLiteral(instant)); + } + + [Fact] + public void GenerateSqlLiteral_returns_instant_infinity_literal() + { + var mapping = GetMapping(typeof(Instant)); + Assert.Equal(typeof(Instant), mapping.ClrType); + Assert.Equal("timestamp without time zone", mapping.StoreType); + + Assert.Equal("TIMESTAMP '-infinity'", mapping.GenerateSqlLiteral(Instant.MinValue)); + Assert.Equal("TIMESTAMP 'infinity'", mapping.GenerateSqlLiteral(Instant.MaxValue)); + } + + [Fact] + public void GenerateSqlLiteral_returns_instant_range_in_legacy_mode() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + Assert.Equal("tsrange", mapping.StoreType); + Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); + + var value = new NpgsqlRange( + new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc().ToInstant()); + Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tsrange", mapping.GenerateSqlLiteral(value)); + } + + #region Support + + private static readonly NpgsqlTypeMappingSource Mapper = new( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + []), + new RelationalTypeMappingSourceDependencies( + new IRelationalTypeMappingSourcePlugin[] + { + new NpgsqlNodaTimeTypeMappingSourcePlugin( + new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies())) + }), + new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new NpgsqlSingletonOptions() + ); + + private static RelationalTypeMapping GetMapping(string storeType) + => Mapper.FindMapping(storeType); + + private static RelationalTypeMapping GetMapping(Type clrType) + => Mapper.FindMapping(clrType); + + private static RelationalTypeMapping GetMapping(Type clrType, string storeType) + => Mapper.FindMapping(clrType, storeType); + + private class LegacyNpgsqlNodaTimeTypeMappingFixture : IDisposable + { + public LegacyNpgsqlNodaTimeTypeMappingFixture() + { + NpgsqlNodaTimeTypeMappingSourcePlugin.LegacyTimestampBehavior = true; + } + + public void Dispose() + => NpgsqlNodaTimeTypeMappingSourcePlugin.LegacyTimestampBehavior = false; + } + + #endregion Support + } + + [CollectionDefinition("LegacyNodaTimeTest", DisableParallelization = true)] + public class EventSourceTestCollection; +} + +#endif diff --git a/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs index 02ec1ffab..13a4dcb34 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs @@ -144,7 +144,7 @@ protected override void Seed(TimestampQueryContext context) } [CollectionDefinition("LegacyTimestampQueryTest", DisableParallelization = true)] - public class EventSourceTestCollection; + public class NodaTimeEventSourceTestCollection; } #endif diff --git a/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs new file mode 100644 index 000000000..c6fa0961b --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs @@ -0,0 +1,2028 @@ +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class NodaTimeQueryNpgsqlTest : QueryTestBase +{ + public NodaTimeQueryNpgsqlTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Operator(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate < new LocalDate(2018, 4, 21))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDate" < DATE '2018-04-21' +"""); + } + + #region Addition and subtraction + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Add_LocalDate_Period(bool async) + { + // Note: requires some special type inference logic because we're adding things of different types + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate + Period.FromMonths(1) > t.LocalDate)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDate" + INTERVAL 'P1M' > n."LocalDate" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_Instant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant + Duration.FromDays(1) - t.Instant == Duration.FromDays(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE (n."Instant" + INTERVAL '1 00:00:00') - n."Instant" = INTERVAL '1 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime + Period.FromDays(1) - t.LocalDateTime == Period.FromDays(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE (n."LocalDateTime" + INTERVAL 'P1D') - n."LocalDateTime" = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_ZonedDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime + Duration.FromDays(1) - t.ZonedDateTime == Duration.FromDays(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE (n."ZonedDateTime" + INTERVAL '1 00:00:00') - n."ZonedDateTime" = INTERVAL '1 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - t.LocalDate == Period.FromDays(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE make_interval(days => n."LocalDate2" - n."LocalDate") = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate_parameter(bool async) + { + var date = new LocalDate(2018, 4, 20); + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - date == Period.FromDays(1))); + + AssertSql( + """ +@__date_0='Friday, 20 April 2018' (DbType = Date) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE make_interval(days => n."LocalDate2" - @__date_0) = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - new LocalDate(2018, 4, 20) == Period.FromDays(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE make_interval(days => n."LocalDate2" - DATE '2018-04-20') = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime + Period.FromHours(1) - t.LocalTime == Period.FromHours(1))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE (n."LocalTime" + INTERVAL 'PT1H') - n."LocalTime" = INTERVAL 'PT1H' +"""); + } + + #endregion + + #region LocalDateTime + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Year == 2018)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('year', n."LocalDateTime")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Month == 4)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('month', n."LocalDateTime")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_DayOfYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.DayOfYear == 110)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('doy', n."LocalDateTime")::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Day == 20)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', n."LocalDateTime")::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Hour == 10)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', n."LocalDateTime")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Minute == 31)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', n."LocalDateTime")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Second == 33)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', n."LocalDateTime"))::int = 33 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Date == new LocalDate(2018, 4, 20))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDateTime"::date = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_Time(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.TimeOfDay == new LocalTime(10, 31, 33, 666))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDateTime"::time = TIME '10:31:33.666' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_DayOfWeek(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.DayOfWeek == IsoDayOfWeek.Friday)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE CASE floor(date_part('dow', n."LocalDateTime"))::int + WHEN 0 THEN 7 + ELSE floor(date_part('dow', n."LocalDateTime"))::int +END = 5 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_InZoneLeniently_ToInstant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@__ToInstant_0='2018-04-20T08:31:33Z' (DbType = DateTime) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDateTime" AT TIME ZONE 'Europe/Berlin' = @__ToInstant_0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime_InZoneLeniently_ToInstant_with_column_time_zone(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb[t.TimeZoneId]).ToInstant() + == new ZonedDateTime( + new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@__ToInstant_0='2018-04-20T08:31:33Z' (DbType = DateTime) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."LocalDateTime" AT TIME ZONE n."TimeZoneId" = @__ToInstant_0 +"""); + } + + [ConditionalFact] + public async Task LocalDateTime_Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes + .OrderBy(t => EF.Functions.Distance(t.LocalDateTime, new LocalDateTime(2018, 4, 1, 0, 0, 0))).FirstAsync(); + + Assert.Equal(1, closest.Id); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +ORDER BY n."LocalDateTime" <-> TIMESTAMP '2018-04-01T00:00:00' NULLS FIRST +LIMIT 1 +"""); + } + + #endregion LocalDateTime + + #region LocalDate + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDate_Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Year == 2018)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('year', n."LocalDate")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDate_Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Month == 4)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('month', n."LocalDate")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDate_DayOrYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.DayOfYear == 110)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('doy', n."LocalDate")::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDate_Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Day == 20)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', n."LocalDate")::int = 20 +"""); + } + + [ConditionalFact] + public async Task LocalDate_Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes.OrderBy(t => EF.Functions.Distance(t.LocalDate, new LocalDate(2018, 4, 1))).FirstAsync(); + + Assert.Equal(1, closest.Id); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +ORDER BY n."LocalDate" <-> DATE '2018-04-01' NULLS FIRST +LIMIT 1 +"""); + } + + #endregion LocalDate + + #region LocalTime + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalTime_Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Hour == 10)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', n."LocalTime")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalTime_Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Minute == 31)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', n."LocalTime")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalTime_Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Second == 33)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', n."LocalTime"))::int = 33 +"""); + } + + #endregion LocalTime + + #region Period + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Years(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Years == 2018)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('year', n."Period")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Months(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Months == 4)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('month', n."Period")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Days(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Days == 20)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', n."Period")::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Hours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Hours == 10)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', n."Period")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Minutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Minutes == 31)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', n."Period")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_Seconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Seconds == 23)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', n."Period"))::int = 23 +"""); + } + + // PostgreSQL does not support extracting weeks from intervals + [ConditionalFact] + public Task Period_Weeks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Weeks == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Period_Milliseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Period_Nanoseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Period_Ticks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Ticks == 0).ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromYears(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromYears(t.Id).Years == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('year', make_interval(years => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromMonths(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMonths(t.Id).Months == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('month', make_interval(months => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromWeeks(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Days == 7), + ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Normalize().Days == 7)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', make_interval(weeks => n."Id"))::int = 7 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromDays(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromDays(t.Id).Days == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', make_interval(days => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromHours_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromHours(t.Id).Hours == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', make_interval(hours => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromHours_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromHours(t.Long).Hours == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', make_interval(hours => n."Long"::int))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromMinutes_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMinutes(t.Id).Minutes == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', make_interval(mins => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromMinutes_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMinutes(t.Long).Minutes == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', make_interval(mins => n."Long"::int))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromSeconds_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromSeconds(t.Id).Seconds == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', make_interval(secs => n."Id"::bigint::double precision)))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Period_FromSeconds_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromSeconds(t.Long).Seconds == 1)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', make_interval(secs => n."Long"::double precision)))::int = 1 +"""); + } + + [ConditionalFact] + public Task Period_FromMilliseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromMilliseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalFact] + public Task Period_FromNanoseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalFact] + public Task Period_FromTicks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Sum_over_Period(bool async) + { + await using var ctx = CreateContext(); + + // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). + var query = ctx.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Sum(g.Select(o => o.Period))); + + _ = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + """ +SELECT sum(n."Period") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Average_over_Period(bool async) + { + await using var ctx = CreateContext(); + + // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). + var query = ctx.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Average(g.Select(o => o.Period))); + + _ = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + """ +SELECT avg(n."Period") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + #endregion Period + + #region Duration + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_TotalDays(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalDays > 27)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('epoch', n."Duration") / 86400.0 > 27.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_TotalHours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalHours < 700)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('epoch', n."Duration") / 3600.0 < 700.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_TotalMinutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalMinutes < 40000)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('epoch', n."Duration") / 60.0 < 40000.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_TotalSeconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalSeconds == 2365448.02)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('epoch', n."Duration") = 2365448.02 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_TotalMilliseconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalMilliseconds == 2365448020)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('epoch', n."Duration") / 0.001 = 2365448020.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_Days(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Days == 27)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', n."Duration")::int = 27 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_Hours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Hours == 9)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', n."Duration")::int = 9 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_Minutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Minutes == 4)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', n."Duration")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Duration_Seconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Seconds == 8)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', n."Duration"))::int = 8 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Sum_over_Duration(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Sum(g.Select(o => o.Duration))), + expectedQuery: ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (Duration?)Duration.FromTicks(g.Sum(o => o.Duration.TotalTicks)))); + + AssertSql( + """ +SELECT sum(n."Duration") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Average_over_Duration(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Average(g.Select(o => o.Duration))), + expectedQuery: ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (Duration?)Duration.FromTicks((long)g.Average(o => o.Duration.TotalTicks)))); + + AssertSql( + """ +SELECT avg(n."Duration") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + #endregion + + #region Interval + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Start(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(t => t.Interval.Start == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666).InUtc().ToInstant())); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE lower(n."Interval") = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_End(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.End == new LocalDateTime(2018, 4, 25, 10, 31, 33, 666).InUtc().ToInstant())); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE upper(n."Interval") = TIMESTAMPTZ '2018-04-25T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_HasStart(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.HasStart)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE NOT (lower_inf(n."Interval")) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_HasEnd(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.HasEnd)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE NOT (upper_inf(n."Interval")) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Duration(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.Duration == Duration.FromDays(5))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE upper(n."Interval") - lower(n."Interval") = INTERVAL '5 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Contains_Instant(bool async) + { + var interval = new Interval( + new LocalDateTime(2018, 01, 01, 0, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 12, 25, 0, 0, 0).InUtc().ToInstant()); + + await AssertQuery( + async, + ss => ss.Set().Where(t => interval.Contains(t.Instant))); + + AssertSql( + """ +@__interval_0='2018-01-01T00:00:00Z/2020-12-25T00:00:00Z' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE @__interval_0 @> n."Instant" +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task Interval_RangeAgg(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeAgg(g.Select(x => x.Interval))); + + var union = async + ? await query.SingleAsync() + : query.Single(); + + var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); + Assert.Equal([new(start, start + Duration.FromDays(5))], union); + + AssertSql( + """ +SELECT range_agg(n0."Interval") +FROM ( + SELECT n."Interval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Intersect_aggregate(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.Interval))); + + var intersection = async + ? await query.SingleAsync() + : query.Single(); + + var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); + Assert.Equal(new Interval(start, start + Duration.FromDays(5)), intersection); + + AssertSql( + """ +SELECT range_intersect_agg(n0."Interval") +FROM ( + SELECT n."Interval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + #endregion Interval + + #region DateInterval + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Length == 5)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE upper(n."DateInterval") - lower(n."DateInterval") = 5 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Start(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Start == new LocalDate(2018, 4, 20))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE lower(n."DateInterval") = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_End(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.End == new LocalDate(2018, 4, 24))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) = DATE '2018-04-24' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_End_Select(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(t => t.DateInterval.End)); + + AssertSql( + """ +SELECT CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) +FROM "NodaTimeTypes" AS n +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Contains_LocalDate(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 01, 01), new LocalDate(2020, 12, 25)); + + await AssertQuery( + async, + ss => ss.Set().Where(t => dateInterval.Contains(t.LocalDate))); + + AssertSql( + """ +@__dateInterval_0='[2018-01-01, 2020-12-25]' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE @__dateInterval_0 @> n."LocalDate" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Contains_DateInterval(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)); + + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Contains(dateInterval))); + + AssertSql( + """ +@__dateInterval_0='[2018-04-22, 2018-04-24]' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."DateInterval" @> @__dateInterval_0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Intersection(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.DateInterval.Intersection(dateInterval) == new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)))); + + AssertSql( + """ +@__dateInterval_0='[2018-04-22, 2018-04-26]' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."DateInterval" * @__dateInterval_0 = '[2018-04-22,2018-04-24]'::daterange +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Union(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.DateInterval.Union(dateInterval) == new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 26)))); + + AssertSql( + """ +@__dateInterval_0='[2018-04-22, 2018-04-26]' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."DateInterval" + @__dateInterval_0 = '[2018-04-20,2018-04-26]'::daterange +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_RangeAgg(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeAgg(g.Select(x => x.DateInterval))); + + var union = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal([new(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24))], union); + + AssertSql( + """ +SELECT range_agg(n0."DateInterval") +FROM ( + SELECT n."DateInterval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task DateInterval_Intersect_aggregate(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.DateInterval))); + + var intersection = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal(new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24)), intersection); + + AssertSql( + """ +SELECT range_intersect_agg(n0."DateInterval") +FROM ( + SELECT n."DateInterval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + #endregion DateInterval + + #region Range + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DateRange_Contains(bool async) + { + var dateRange = new DateInterval(new LocalDate(2018, 01, 01), new LocalDate(2020, 12, 26)); + + await AssertQuery( + async, + ss => ss.Set().Where(t => dateRange.Contains(t.LocalDate))); + + AssertSql( + """ +@__dateRange_0='[2018-01-01, 2020-12-26]' (DbType = Object) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE @__dateRange_0 @> n."LocalDate" +"""); + } + + #endregion Range + + #region Instant + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_InUtc(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InUtc() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero))); + + AssertSql( + """ +@__p_0='2018-04-20T10:31:33 UTC (+00)' (DbType = DateTime) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" = @__p_0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_InZone_constant_LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).LocalDateTime + == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" AT TIME ZONE 'Europe/Berlin' = TIMESTAMP '2018-04-20T12:31:33.666' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_InZone_constant_Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).Date + == new LocalDate(2018, 4, 20))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE CAST(n."Instant" AT TIME ZONE 'Europe/Berlin' AS date) = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_InZone_parameter_LocalDateTime(bool async) + { + var timeZone = DateTimeZoneProviders.Tzdb["Europe/Berlin"]; + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(timeZone).LocalDateTime + == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); + + AssertSql( + """ +@__timeZone_0='Europe/Berlin' + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" AT TIME ZONE @__timeZone_0 = TIMESTAMP '2018-04-20T12:31:33.666' +"""); + } + + [ConditionalFact] + public async Task Instance_InZone_without_LocalDateTime_fails() + { + await using var ctx = CreateContext(); + + await Assert.ThrowsAsync( + () => ctx.Set().Where(t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]) == default) + .ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Instance_ToDateTimeUtc(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(t => t.Instant.ToDateTimeUtc() == new DateTime(2018, 4, 20, 10, 31, 33, 666, DateTimeKind.Utc))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant"::timestamptz = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GetCurrentInstant_from_Instance(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant < SystemClock.Instance.GetCurrentInstant())); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" < NOW() +"""); + } + + [ConditionalFact] + public async Task Instant_Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes + .OrderBy(t => EF.Functions.Distance(t.Instant, new LocalDateTime(2018, 4, 1, 0, 0, 0).InUtc().ToInstant())).FirstAsync(); + + Assert.Equal(1, closest.Id); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +ORDER BY n."Instant" <-> TIMESTAMPTZ '2018-04-01T00:00:00Z' NULLS FIRST +LIMIT 1 +"""); + } + + #endregion + + #region ZonedDateTime + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Year == 2018)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('year', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Month == 4)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('month', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_DayOfYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.DayOfYear == 110)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('doy', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Day == 20)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('day', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Hour == 10)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('hour', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Minute == 31)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE date_part('minute', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Second == 33)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE floor(date_part('second', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int = 33 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Date == new LocalDate(2018, 4, 20))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE CAST(n."ZonedDateTime" AT TIME ZONE 'UTC' AS date) = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_DayOfWeek(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.DayOfWeek == IsoDayOfWeek.Friday)); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE CASE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int + WHEN 0 THEN 7 + ELSE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int +END = 5 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant.InUtc().LocalDateTime == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666))); + + AssertSql( + """ +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."Instant" AT TIME ZONE 'UTC' = TIMESTAMP '2018-04-20T10:31:33.666' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ZonedDateTime_ToInstant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.ZonedDateTime.ToInstant() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@__ToInstant_0='2018-04-20T10:31:33Z' (DbType = DateTime) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +WHERE n."ZonedDateTime" = @__ToInstant_0 +"""); + } + + [ConditionalFact] + public async Task ZonedDateTime_Distance() + { + await using var context = CreateContext(); + + var closest = await context.NodaTimeTypes + .OrderBy( + t => EF.Functions.Distance( + t.ZonedDateTime, + new ZonedDateTime(new LocalDateTime(2018, 4, 1, 0, 0, 0), DateTimeZone.Utc, Offset.Zero))).FirstAsync(); + Assert.Equal(1, closest.Id); + + AssertSql( + """ +@__p_1='2018-04-01T00:00:00 UTC (+00)' (DbType = DateTime) + +SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" +FROM "NodaTimeTypes" AS n +ORDER BY n."ZonedDateTime" <-> @__p_1 NULLS FIRST +LIMIT 1 +"""); + } + + #endregion ZonedDateTime + + #region Support + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private static readonly Period _defaultPeriod = Period.FromYears(2018) + + Period.FromMonths(4) + + Period.FromDays(20) + + Period.FromHours(10) + + Period.FromMinutes(31) + + Period.FromSeconds(23) + + Period.FromMilliseconds(666); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class NodaTimeContext(DbContextOptions options) : PoolableDbContext(options) + { + // ReSharper disable once MemberHidesStaticFromOuterClass + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public DbSet NodaTimeTypes { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasPostgresExtension("btree_gist"); + } + + public static void Seed(NodaTimeContext context) + { + context.AddRange(NodaTimeData.CreateNodaTimeTypes()); + context.SaveChanges(); + } + } + + public class NodaTimeTypes + { + // ReSharper disable UnusedAutoPropertyAccessor.Global + public int Id { get; set; } + public Instant Instant { get; set; } + public LocalDateTime LocalDateTime { get; set; } + public ZonedDateTime ZonedDateTime { get; set; } + public LocalDate LocalDate { get; set; } + public LocalDate LocalDate2 { get; set; } + public LocalTime LocalTime { get; set; } + public OffsetTime OffsetTime { get; set; } + public Period Period { get; set; } + public Duration Duration { get; set; } + public DateInterval DateInterval { get; set; } + public NpgsqlRange LocalDateRange { get; set; } + public Interval Interval { get; set; } + public NpgsqlRange InstantRange { get; set; } + public long Long { get; set; } + + public string TimeZoneId { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global + } + + public class NodaTimeQueryNpgsqlFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory + { + protected override string StoreName + => "NodaTimeQueryTest"; + + // Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account + // don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed + // don't. + // We also instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. + protected override ITestStoreFactory TestStoreFactory + => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + private NodaTimeData _expectedData; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNodaTime(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); + + return optionsBuilder; + } + + protected override void Seed(NodaTimeContext context) + => NodaTimeContext.Seed(context); + + public Func GetContextCreator() + => CreateContext; + + public ISetSource GetExpectedData() + => _expectedData ??= new NodaTimeData(); + + public IReadOnlyDictionary EntitySorters + => new Dictionary> { { typeof(NodaTimeTypes), e => ((NodaTimeTypes)e)?.Id } } + .ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters + => new Dictionary> + { + { + typeof(NodaTimeTypes), (e, a) => + { + Assert.Equal(e is null, a is null); + if (a is not null) + { + var ee = (NodaTimeTypes)e; + var aa = (NodaTimeTypes)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.LocalDateTime, aa.LocalDateTime); + Assert.Equal(ee.ZonedDateTime, aa.ZonedDateTime); + Assert.Equal(ee.Instant, aa.Instant); + Assert.Equal(ee.LocalDate, aa.LocalDate); + Assert.Equal(ee.LocalDate2, aa.LocalDate2); + Assert.Equal(ee.LocalTime, aa.LocalTime); + Assert.Equal(ee.OffsetTime, aa.OffsetTime); + Assert.Equal(ee.Period, aa.Period); + Assert.Equal(ee.Duration, aa.Duration); + Assert.Equal(ee.DateInterval, aa.DateInterval); + // Assert.Equal(ee.DateRange, aa.DateRange); + Assert.Equal(ee.Long, aa.Long); + Assert.Equal(ee.TimeZoneId, aa.TimeZoneId); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); + } + + private class NodaTimeData : ISetSource + { + private IReadOnlyList NodaTimeTypes { get; } = CreateNodaTimeTypes(); + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(NodaTimeTypes)) + { + return (IQueryable)NodaTimeTypes.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + public static IReadOnlyList CreateNodaTimeTypes() + { + var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666); + var zonedDateTime = localDateTime.InUtc(); + var instant = zonedDateTime.ToInstant(); + var duration = Duration.FromMilliseconds(20) + .Plus(Duration.FromSeconds(8)) + .Plus(Duration.FromMinutes(4)) + .Plus(Duration.FromHours(9)) + .Plus(Duration.FromDays(27)); + + return new List + { + new() + { + Id = 1, + LocalDateTime = localDateTime, + ZonedDateTime = zonedDateTime, + Instant = instant, + LocalDate = localDateTime.Date, + LocalDate2 = localDateTime.Date + Period.FromDays(1), + LocalTime = localDateTime.TimeOfDay, + OffsetTime = new OffsetTime(new LocalTime(10, 31, 33, 666), Offset.Zero), + Period = _defaultPeriod, + Duration = duration, + DateInterval = new DateInterval(localDateTime.Date, localDateTime.Date.PlusDays(4)), // inclusive + LocalDateRange = new NpgsqlRange(localDateTime.Date, localDateTime.Date.PlusDays(5)), // exclusive + Interval = new Interval(instant, instant + Duration.FromDays(5)), + InstantRange = new NpgsqlRange(instant, true, instant + Duration.FromDays(5), false), + Long = 1, + TimeZoneId = "Europe/Berlin" + } + }; + } + } + + #endregion Support +} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs index dbe69d633..05f61b936 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs @@ -4,15 +4,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; public class SpatialQueryNpgsqlFixture : SpatialQueryRelationalFixture { -#pragma warning disable CS0618 // GlobalTypeMapper is obsolete - public SpatialQueryNpgsqlFixture() - { - NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(); - } -#pragma warning restore CS0618 - + // We instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => new NpgsqlTestStoreFactory(useConnectionString: true); protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNetTopologySuite(); diff --git a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs index 4721eba93..2feac4876 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs @@ -869,7 +869,7 @@ protected override string StoreName // don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed // don't. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.WithConnectionStringOptions("-c TimeZone=Europe/Berlin"); + => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin"); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs index cf6c900e3..71a311908 100644 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs @@ -5,15 +5,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class SpatialNpgsqlFixture : SpatialFixtureBase { -#pragma warning disable CS0618 // GlobalTypeMapper is obsolete - public SpatialNpgsqlFixture() - { - NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(); - } -#pragma warning restore CS0618 - + // We instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNetTopologySuite() to function properly and instantiate an NpgsqlDataSource internally. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => new NpgsqlTestStoreFactory(useConnectionString: true); protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection) diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs index 8f610757f..32e4e50e7 100644 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs @@ -7,4 +7,9 @@ public class SpatialNpgsqlTest(SpatialNpgsqlFixture fixture) : SpatialTestBase facade.UseTransaction(transaction.GetDbTransaction()); + + // This test requires DbConnection to be used with the test store, but SpatialNpgsqlFixture must set useConnectionString to true + // in order to properly set up the NetTopologySuite internally with the data source. + public override void Mutation_of_tracked_values_does_not_mutate_values_in_store() + => Assert.Throws(() => base.Mutation_of_tracked_values_does_not_mutate_values_in_store()); } diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs index fb23e9446..8feb4a2b4 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs @@ -21,8 +21,9 @@ protected NpgsqlNorthwindTestStoreFactory() public override TestStore GetOrCreate(string storeName) => NpgsqlTestStore.GetOrCreate( - Name, "Northwind.sql", - TestEnvironment.PostgresVersion >= new Version(12, 0) - ? @"CREATE COLLATION IF NOT EXISTS ""some-case-insensitive-collation"" (LOCALE = 'en-u-ks-primary', PROVIDER = icu, DETERMINISTIC = False);" + Name, + scriptPath: "Northwind.sql", + additionalSql: TestEnvironment.PostgresVersion >= new Version(12, 0) + ? """CREATE COLLATION IF NOT EXISTS "some-case-insensitive-collation" (LOCALE = 'en-u-ks-primary', PROVIDER = icu, DETERMINISTIC = False);""" : null); } diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs index c6acdf2f5..7ed735a7f 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs @@ -1,6 +1,8 @@ using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; @@ -10,6 +12,7 @@ public class NpgsqlTestStore : RelationalTestStore { private readonly string? _scriptPath; private readonly string? _additionalSql; + private readonly string? _connectionString; private const string Northwind = "Northwind"; @@ -29,8 +32,9 @@ public static NpgsqlTestStore GetOrCreate( string name, string? scriptPath = null, string? additionalSql = null, - string? connectionStringOptions = null) - => new(name, scriptPath, additionalSql, connectionStringOptions); + string? connectionStringOptions = null, + bool useConnectionString = false) + => new(name, scriptPath, additionalSql, connectionStringOptions, useConnectionString: useConnectionString); public static NpgsqlTestStore Create(string name, string? connectionStringOptions = null) => new(name, connectionStringOptions: connectionStringOptions, shared: false); @@ -39,14 +43,20 @@ public static NpgsqlTestStore CreateInitialized(string name) => new NpgsqlTestStore(name, shared: false) .InitializeNpgsql(null, (Func?)null, null); - private NpgsqlTestStore( + public NpgsqlTestStore( string name, string? scriptPath = null, string? additionalSql = null, string? connectionStringOptions = null, - bool shared = true) + bool shared = true, + bool useConnectionString = false) : base(name, shared, CreateConnection(name, connectionStringOptions)) { + if (useConnectionString) + { + _connectionString = CreateConnectionString(name, connectionStringOptions); + } + Name = name; if (scriptPath is not null) @@ -104,23 +114,19 @@ protected override void Initialize(Func createContext, Action builder.UseNpgsql( - Connection, b => b.ApplyConfiguration() - .CommandTimeout(CommandTimeout) - // The tests are written with the assumption that NULLs are sorted first (SQL Server and .NET behavior), but PostgreSQL - // sorts NULLs last by default. This configures the provider to emit NULLS FIRST. - .ReverseNullOrdering()); - - private static string GetScratchDbName() { - string name; - do - { - name = "Scratch_" + Guid.NewGuid(); - } - while (DatabaseExists(name)); - - return name; + Action npgsqlOptionsBuilder = b => b.ApplyConfiguration() + .CommandTimeout(CommandTimeout) + // The tests are written with the assumption that NULLs are sorted first (SQL Server and .NET behavior), but PostgreSQL + // sorts NULLs last by default. This configures the provider to emit NULLS FIRST. + .ReverseNullOrdering(); + + // The default mode in the EF tests is to use a DbConnection, but in Npgsql we have certain test suites which require that + // we use a connection string instead, because an NpgsqlDataSource is required internally (e.g. enums or plugins + // are used). + return _connectionString is null + ? builder.UseNpgsql(Connection, npgsqlOptionsBuilder) + : builder.UseNpgsql(_connectionString, npgsqlOptionsBuilder); } private bool CreateDatabase(Action? clean) diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs index 95c16a849..06de22f29 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs @@ -1,26 +1,21 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -public class NpgsqlTestStoreFactory : RelationalTestStoreFactory -{ - private readonly string _connectionStringOptions; +#nullable enable +public class NpgsqlTestStoreFactory( + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, + bool useConnectionString = false) : RelationalTestStoreFactory +{ public static NpgsqlTestStoreFactory Instance { get; } = new(); - public static NpgsqlTestStoreFactory WithConnectionStringOptions(string connectionStringOptions) - => new(connectionStringOptions); - - protected NpgsqlTestStoreFactory(string connectionStringOptions = null) - { - _connectionStringOptions = connectionStringOptions; - } - public override TestStore Create(string storeName) - => NpgsqlTestStore.Create(storeName, _connectionStringOptions); + => new NpgsqlTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: false, useConnectionString); public override TestStore GetOrCreate(string storeName) - => NpgsqlTestStore.GetOrCreate(storeName, connectionStringOptions: _connectionStringOptions); + => new NpgsqlTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: true, useConnectionString); public override IServiceCollection AddProviderServices(IServiceCollection serviceCollection) => serviceCollection.AddEntityFrameworkNpgsql(); - // .AddEntityFrameworkNpgsqlNetTopologySuite(); } diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 1fab81f8c..ec9815e04 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -1,6 +1,5 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using Xunit.Sdk; namespace Npgsql.EntityFrameworkCore.PostgreSQL; @@ -1904,18 +1903,13 @@ public class NodaTimeQueryNpgsqlFixture : SharedStoreFixtureBase "NodaTimeTest"; -#pragma warning disable CS0618 // GlobalTypeMapper is obsolete - public NodaTimeQueryNpgsqlFixture() - { - NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); - } -#pragma warning restore CS0618 - // Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account // don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed // don't. + // We also instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.WithConnectionStringOptions("-c TimeZone=Europe/Berlin"); + => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs b/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs index 516229e48..e42956b57 100644 --- a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs @@ -83,7 +83,7 @@ public void Can_add_extension_with_connection_using_generic_options() [ConditionalTheory] [InlineData(false)] [InlineData(true)] - public void Service_collection_extension_method_can_configure_sqlserver_options(bool nullConnectionString) + public void Service_collection_extension_method_can_configure_npgsql_options(bool nullConnectionString) { var serviceCollection = new ServiceCollection(); serviceCollection.AddNpgsql( diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs index 7f6421338..e5ce3d183 100644 --- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; @@ -14,7 +15,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class NpgsqlRelationalConnectionTest { [Fact] - public void Creates_Npgsql_Server_connection_string() + public void Creates_NpgsqlConnection() { using var connection = CreateConnection(); @@ -51,7 +52,7 @@ public void Uses_DbDataSource_from_DbContextOptions() Assert.Equal("Host=FakeHost", connection2.ConnectionString); } - [Fact(Skip = "Passes in isolation, but fails when the entire test suite is run because of #2891")] + [Fact] public void Uses_DbDataSource_from_application_service_provider() { var serviceCollection = new ServiceCollection(); @@ -101,6 +102,83 @@ public void DbDataSource_from_application_service_provider_does_not_used_if_conn Assert.Equal("Host=FakeHost2", connection1.ConnectionString); } + [Fact] + public void Multiple_connection_strings_with_plugin() + { + var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withNetTopologySuite: true); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource); + } + + [Fact] + public void Multiple_connection_strings_with_enum() + { + var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withEnum: true); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource); + } + + [Fact] + public void Multiple_connection_strings_without_data_source_features() + { + var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1"); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Null(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1"); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Null(connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2"); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.Null(connection3.DbDataSource); + } + + private class ConnectionStringSwitchingContext(string connectionString, bool withNetTopologySuite = false, bool withEnum = false) + : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql(connectionString, b => + { + if (withNetTopologySuite) + { + b.UseNetTopologySuite(); + } + + if (withEnum) + { + b.MapEnum("mood"); + } + }); + + private enum Mood { Happy, Sad } + } + [Fact] public void Can_create_master_connection_with_connection_string() { @@ -193,8 +271,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio extension.Validate(options); } - var singletonOptions = new NpgsqlSingletonOptions(); - singletonOptions.Initialize(options); + var dbContextOptions = CreateOptions(); return new NpgsqlRelationalConnection( new RelationalConnectionDependencies( @@ -211,7 +288,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio new DiagnosticListener("FakeDiagnosticListener"), new NpgsqlLoggingDefinitions(), new NullDbContextLogger(), - CreateOptions()), + dbContextOptions), new NamedConnectionStringResolver(options), new RelationalTransactionFactory( new RelationalTransactionFactoryDependencies( @@ -226,7 +303,8 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), new NpgsqlSingletonOptions()), new ExceptionDetector()))), - singletonOptions); + new NpgsqlDataSourceManager([]), + dbContextOptions); } private const string ConnectionString = "Fake Connection String"; diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs similarity index 99% rename from test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs rename to test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs index f2cb829af..4fe8cc00a 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs @@ -1,6 +1,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; +using NodaTime; using NodaTime.Calendars; using NodaTime.Text; using NodaTime.TimeZones; @@ -8,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -namespace Npgsql.EntityFrameworkCore.PostgreSQL; +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; public class NpgsqlNodaTimeTypeMappingTest { diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs index c16e5d2d2..c735a165c 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs @@ -312,7 +312,7 @@ private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion new RelationalTypeMappingSourceDependencies( new IRelationalTypeMappingSourcePlugin[] { - new NpgsqlNetTopologySuiteTypeMappingSourcePlugin(new NpgsqlNetTopologySuiteOptions()), + new NpgsqlNetTopologySuiteTypeMappingSourcePlugin(new NpgsqlNetTopologySuiteSingletonOptions()), new DummyTypeMappingSourcePlugin() }), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index 77f45d8bf..c2d90bd2a 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -489,7 +489,11 @@ public void ValueComparer_hstore_as_ImmutableDictionary() [Fact] public void GenerateSqlLiteral_returns_enum_literal() { - var mapping = new NpgsqlEnumTypeMapping("dummy_enum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator()); + var mapping = new NpgsqlEnumTypeMapping("dummy_enum", "dummy_enum", typeof(DummyEnum), new Dictionary + { + [DummyEnum.Happy] = "happy", + [DummyEnum.Sad] = "sad" + }); Assert.Equal("'sad'::dummy_enum", mapping.GenerateSqlLiteral(DummyEnum.Sad)); } @@ -497,7 +501,11 @@ public void GenerateSqlLiteral_returns_enum_literal() [Fact] public void GenerateSqlLiteral_returns_enum_uppercase_literal() { - var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator()); + var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", "DummyEnum", typeof(DummyEnum), new Dictionary + { + [DummyEnum.Happy] = "happy", + [DummyEnum.Sad] = "sad" + }); Assert.Equal(@"'sad'::""DummyEnum""", mapping.GenerateSqlLiteral(DummyEnum.Sad)); }