diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76cf1f8ea..d6a5ceaa7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: pull_request: env: - dotnet_sdk_version: '8.0.100' + dotnet_sdk_version: '9.0.100' postgis_version: 3 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true @@ -23,11 +23,11 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, windows-2022] - pg_major: [16, 15, 14, 13, 12, 11] + pg_major: [17, 16, 15, 14, 13] config: [Release] include: - os: ubuntu-22.04 - pg_major: 16 + pg_major: 17 config: Debug # - os: ubuntu-22.04 # pg_major: 17 @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -152,7 +152,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -160,7 +160,7 @@ jobs: run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" -p:ContinuousIntegrationBuild=true - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: EFCore.PG.CI path: nupkgs @@ -186,7 +186,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -194,7 +194,7 @@ jobs: run: dotnet pack --configuration Release --property:PackageOutputPath="$PWD/nupkgs" -p:ContinuousIntegrationBuild=true - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: EFCore.PG.Release path: nupkgs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 647d3a05b..c5e68a34c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,7 +27,7 @@ on: - cron: '30 22 * * 6' env: - dotnet_sdk_version: '8.0.100' + dotnet_sdk_version: '9.0.100' jobs: analyze: @@ -51,7 +51,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3.0.3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.dotnet_sdk_version }} @@ -84,4 +84,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/Directory.Build.props b/Directory.Build.props index c6c87c2bc..9d7160b71 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 8.0.0 - preview + 9.0.4 + latest true latest NU5105 @@ -12,7 +12,7 @@ true true - Copyright 2023 © The Npgsql Development Team + Copyright 2024 © The Npgsql Development Team Npgsql true PostgreSQL diff --git a/Directory.Packages.props b/Directory.Packages.props index 19b7d4b62..5a263de4f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,8 +1,8 @@ - 8.0.0 - 8.0.0 - 8.0.0 + [9.0.1,10.0.0) + 9.0.1 + 9.0.3 @@ -23,8 +23,10 @@ - - + + + + diff --git a/EFCore.PG.sln b/EFCore.PG.sln index d1e61fc97..9ccd15d74 100644 --- a/EFCore.PG.sln +++ b/EFCore.PG.sln @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md NuGet.config = NuGet.config global.json = global.json + .editorconfig = .editorconfig EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}" @@ -22,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}" @@ -69,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 @@ -113,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/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 06af66077..ff5652c30 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -182,212 +182,12 @@ True True - - True - True - True - Side by side - Side by side - False - False - False - True - False - False - True - False - False - False - True - <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> - True - FK - MARS - $object$_On$event$ - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Methods"><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="Aa_bb" /></Policy> - EF - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> - $object$_On$event$ - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - - - True - C:\repos\EntityFrameworkCore\All.sln.DotSettings - - - - True - 2 - DO_NOTHING - True - True True - True True - True True - True True True - True - True - True - True True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True True - True - True - True - True - True - True - True - True - True - True - True True - True - True - True - True - True - True - True - True - True - True - True - True + \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 6ee73d258..834023c22 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,11 +1,9 @@ - @@ -20,11 +18,9 @@ - diff --git a/global.json b/global.json index 817f0c380..733b653c1 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "8.0.100-rc.2.23502.2", + "version": "9.0.100", "rollForward": "latestMajor", - "allowPrerelease": "true" + "allowPrerelease": false } } 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..e8b06a835 --- /dev/null +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs @@ -0,0 +1,24 @@ +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 9de048388..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 @@ -109,15 +179,10 @@ public virtual void Validate(IDbContextOptions options) } } - private sealed class ExtensionInfo : DbContextOptionsExtensionInfo + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) { private string? _logFragment; - public ExtensionInfo(IDbContextOptionsExtension extension) - : base(extension) - { - } - private new NpgsqlNetTopologySuiteOptionsExtension Extension => (NpgsqlNetTopologySuiteOptionsExtension)base.Extension; @@ -128,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/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs index 6843da9df..86de97978 100644 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs @@ -28,10 +28,10 @@ public NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin( throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); } - Translators = new IAggregateMethodCallTranslator[] - { + Translators = + [ new NpgsqlNetTopologySuiteAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) - }; + ]; } /// @@ -52,16 +52,16 @@ public NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin( public class NpgsqlNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator { private static readonly MethodInfo GeometryCombineMethod - = typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), new[] { typeof(IEnumerable) })!; + = typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), [typeof(IEnumerable)])!; private static readonly MethodInfo ConvexHullMethod - = typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), new[] { typeof(IEnumerable) })!; + = typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), [typeof(IEnumerable)])!; private static readonly MethodInfo UnionMethod - = typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable) })!; + = typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), [typeof(IEnumerable)])!; private static readonly MethodInfo EnvelopeCombineMethod - = typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), new[] { typeof(IEnumerable) })!; + = typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), [typeof(IEnumerable)])!; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -103,19 +103,18 @@ public NpgsqlNetTopologySuiteAggregateMethodTranslator( // https://postgis.net/docs/ST_ConvexHull.html return _sqlExpressionFactory.Function( "ST_ConvexHull", - new[] - { + [ _sqlExpressionFactory.AggregateFunction( "ST_Collect", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, - argumentsPropagateNullability: new[] { false }, + argumentsPropagateNullability: [false], typeof(Geometry), GetMapping()) - }, + ], nullable: true, - argumentsPropagateNullability: new[] { true }, + argumentsPropagateNullability: [true], typeof(Geometry), GetMapping()); } @@ -127,10 +126,10 @@ public NpgsqlNetTopologySuiteAggregateMethodTranslator( return _sqlExpressionFactory.Convert( _sqlExpressionFactory.AggregateFunction( "ST_Extent", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, - argumentsPropagateNullability: new[] { false }, + argumentsPropagateNullability: [false], typeof(Geometry), GetMapping()), typeof(Geometry), GetMapping()); @@ -140,10 +139,10 @@ public NpgsqlNetTopologySuiteAggregateMethodTranslator( { return _sqlExpressionFactory.AggregateFunction( method == UnionMethod ? "ST_Union" : "ST_Collect", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, - argumentsPropagateNullability: new[] { false }, + argumentsPropagateNullability: [false], typeof(Geometry), GetMapping()); } diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs index 95726cb29..e9fb5ac9f 100644 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs @@ -20,7 +20,7 @@ public NpgsqlNetTopologySuiteMemberTranslatorPlugin( IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) { - Translators = new IMemberTranslator[] { new NpgsqlGeometryMemberTranslator(sqlExpressionFactory, typeMappingSource), }; + Translators = [new NpgsqlGeometryMemberTranslator(sqlExpressionFactory, typeMappingSource)]; } /// @@ -44,7 +44,7 @@ public class NpgsqlGeometryMemberTranslator : IMemberTranslator private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly CaseWhenClause[] _ogcGeometryTypeWhenThenList; - private static readonly bool[][] TrueArrays = { Array.Empty(), new[] { true }, new[] { true, true }, new[] { true, true, true } }; + private static readonly bool[][] TrueArrays = [[], [true], [true, true], [true, true, true]]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -59,8 +59,8 @@ public NpgsqlGeometryMemberTranslator( _sqlExpressionFactory = sqlExpressionFactory; _typeMappingSource = typeMappingSource; - _ogcGeometryTypeWhenThenList = new[] - { + _ogcGeometryTypeWhenThenList = + [ new CaseWhenClause( _sqlExpressionFactory.Constant("ST_CircularString"), _sqlExpressionFactory.Constant(OgcGeometryType.CircularString)), new CaseWhenClause( @@ -88,7 +88,7 @@ public NpgsqlGeometryMemberTranslator( _sqlExpressionFactory.Constant("ST_PolyhedralSurface"), _sqlExpressionFactory.Constant(OgcGeometryType.PolyhedralSurface)), new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Tin"), _sqlExpressionFactory.Constant(OgcGeometryType.TIN)) - }; + ]; } /// @@ -127,7 +127,7 @@ public NpgsqlGeometryMemberTranslator( if (function is not null) { - return Function(function, new[] { instance }, typeof(double)); + return Function(function, [instance], typeof(double)); } } @@ -135,44 +135,44 @@ public NpgsqlGeometryMemberTranslator( { if (member.Name == "Count") { - return Function("ST_NumPoints", new[] { instance }, typeof(int)); + return Function("ST_NumPoints", [instance], typeof(int)); } } return member.Name switch { - nameof(Geometry.Area) => Function("ST_Area", new[] { instance }, typeof(double)), - nameof(Geometry.Boundary) => Function("ST_Boundary", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.Centroid) => Function("ST_Centroid", new[] { instance }, typeof(Point), ResultGeometryMapping()), - nameof(GeometryCollection.Count) => Function("ST_NumGeometries", new[] { instance }, typeof(int)), - nameof(Geometry.Dimension) => Function("ST_Dimension", new[] { instance }, typeof(Dimension)), - nameof(LineString.EndPoint) => Function("ST_EndPoint", new[] { instance }, typeof(Point), ResultGeometryMapping()), - nameof(Geometry.Envelope) => Function("ST_Envelope", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), - nameof(Polygon.ExteriorRing) => Function("ST_ExteriorRing", new[] { instance }, typeof(LineString), ResultGeometryMapping()), - nameof(Geometry.GeometryType) => Function("GeometryType", new[] { instance }, typeof(string)), - nameof(LineString.IsClosed) => Function("ST_IsClosed", new[] { instance }, typeof(bool)), - nameof(Geometry.IsEmpty) => Function("ST_IsEmpty", new[] { instance }, typeof(bool)), - nameof(LineString.IsRing) => Function("ST_IsRing", new[] { instance }, typeof(bool)), - nameof(Geometry.IsSimple) => Function("ST_IsSimple", new[] { instance }, typeof(bool)), - nameof(Geometry.IsValid) => Function("ST_IsValid", new[] { instance }, typeof(bool)), - nameof(Geometry.Length) => Function("ST_Length", new[] { instance }, typeof(double)), - nameof(Geometry.NumGeometries) => Function("ST_NumGeometries", new[] { instance }, typeof(int)), - nameof(Polygon.NumInteriorRings) => Function("ST_NumInteriorRings", new[] { instance }, typeof(int)), - nameof(Geometry.NumPoints) => Function("ST_NumPoints", new[] { instance }, typeof(int)), - nameof(Geometry.PointOnSurface) => Function("ST_PointOnSurface", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.InteriorPoint) => Function("ST_PointOnSurface", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.SRID) => Function("ST_SRID", new[] { instance }, typeof(int)), - nameof(LineString.StartPoint) => Function("ST_StartPoint", new[] { instance }, typeof(Point), ResultGeometryMapping()), + nameof(Geometry.Area) => Function("ST_Area", [instance], typeof(double)), + nameof(Geometry.Boundary) => Function("ST_Boundary", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.Centroid) => Function("ST_Centroid", [instance], typeof(Point), ResultGeometryMapping()), + nameof(GeometryCollection.Count) => Function("ST_NumGeometries", [instance], typeof(int)), + nameof(Geometry.Dimension) => Function("ST_Dimension", [instance], typeof(Dimension)), + nameof(LineString.EndPoint) => Function("ST_EndPoint", [instance], typeof(Point), ResultGeometryMapping()), + nameof(Geometry.Envelope) => Function("ST_Envelope", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Polygon.ExteriorRing) => Function("ST_ExteriorRing", [instance], typeof(LineString), ResultGeometryMapping()), + nameof(Geometry.GeometryType) => Function("GeometryType", [instance], typeof(string)), + nameof(LineString.IsClosed) => Function("ST_IsClosed", [instance], typeof(bool)), + nameof(Geometry.IsEmpty) => Function("ST_IsEmpty", [instance], typeof(bool)), + nameof(LineString.IsRing) => Function("ST_IsRing", [instance], typeof(bool)), + nameof(Geometry.IsSimple) => Function("ST_IsSimple", [instance], typeof(bool)), + nameof(Geometry.IsValid) => Function("ST_IsValid", [instance], typeof(bool)), + nameof(Geometry.Length) => Function("ST_Length", [instance], typeof(double)), + nameof(Geometry.NumGeometries) => Function("ST_NumGeometries", [instance], typeof(int)), + nameof(Polygon.NumInteriorRings) => Function("ST_NumInteriorRings", [instance], typeof(int)), + nameof(Geometry.NumPoints) => Function("ST_NumPoints", [instance], typeof(int)), + nameof(Geometry.PointOnSurface) => Function("ST_PointOnSurface", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.InteriorPoint) => Function("ST_PointOnSurface", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.SRID) => Function("ST_SRID", [instance], typeof(int)), + nameof(LineString.StartPoint) => Function("ST_StartPoint", [instance], typeof(Point), ResultGeometryMapping()), nameof(Geometry.OgcGeometryType) => _sqlExpressionFactory.Case( - Function("ST_GeometryType", new[] { instance }, typeof(string)), + Function("ST_GeometryType", [instance], typeof(string)), _ogcGeometryTypeWhenThenList, elseResult: null), _ => null }; - SqlFunctionExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) + SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) => _sqlExpressionFactory.Function( name, arguments, nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs index fc0746aaf..896589719 100644 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -27,7 +27,7 @@ public NpgsqlNetTopologySuiteMethodCallTranslatorPlugin( throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); } - Translators = new IMethodCallTranslator[] { new NpgsqlGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource), }; + Translators = [new NpgsqlGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource)]; } /// @@ -53,9 +53,9 @@ public class NpgsqlGeometryMethodTranslator : IMethodCallTranslator private readonly IRelationalTypeMappingSource _typeMappingSource; private static readonly bool[][] TrueArrays = - { - Array.Empty(), new[] { true }, new[] { true, true }, new[] { true, true, true }, new[] { true, true, true, true } - }; + [ + [], [true], [true, true], [true, true, true], [true, true, true, true] + ]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -90,7 +90,7 @@ public NpgsqlGeometryMethodTranslator( { nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Transform) => _sqlExpressionFactory.Function( "ST_Transform", - new[] { arguments[1], arguments[2] }, + [arguments[1], arguments[2]], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType, @@ -98,7 +98,7 @@ public NpgsqlGeometryMethodTranslator( nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Force2D) => _sqlExpressionFactory.Function( "ST_Force2D", - new[] { arguments[1] }, + [arguments[1]], nullable: true, TrueArrays[1], method.ReturnType, @@ -110,9 +110,9 @@ public NpgsqlGeometryMethodTranslator( arguments[2]), nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Distance) => - TranslateGeometryMethod(arguments[1], method, new[] { arguments[2], arguments[3] }), + TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3]]), nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.IsWithinDistance) => - TranslateGeometryMethod(arguments[1], method, new[] { arguments[2], arguments[3], arguments[4] }), + TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3], arguments[4]]), _ => null }; @@ -147,7 +147,7 @@ public NpgsqlGeometryMethodTranslator( { return _sqlExpressionFactory.Function( "ST_GeometryN", - new[] { instance, OneBased(arguments[0]) }, + [instance, OneBased(arguments[0])], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType, @@ -157,70 +157,70 @@ public NpgsqlGeometryMethodTranslator( return method.Name switch { nameof(Geometry.AsBinary) - => Function("ST_AsBinary", new[] { instance }, typeof(byte[])), + => Function("ST_AsBinary", [instance], typeof(byte[])), nameof(Geometry.AsText) - => Function("ST_AsText", new[] { instance }, typeof(string)), + => Function("ST_AsText", [instance], typeof(string)), nameof(Geometry.Buffer) => Function("ST_Buffer", new[] { instance }.Concat(arguments).ToArray(), typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Contains) - => Function("ST_Contains", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Contains", [instance, arguments[0]], typeof(bool)), nameof(Geometry.ConvexHull) - => Function("ST_ConvexHull", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_ConvexHull", [instance], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.CoveredBy) - => Function("ST_CoveredBy", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_CoveredBy", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Covers) - => Function("ST_Covers", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Covers", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Crosses) - => Function("ST_Crosses", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Crosses", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Disjoint) - => Function("ST_Disjoint", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Disjoint", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Difference) - => Function("ST_Difference", new[] { instance, arguments[0] }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_Difference", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Distance) => Function("ST_Distance", new[] { instance }.Concat(arguments).ToArray(), typeof(double)), nameof(Geometry.EqualsExact) - => Function("ST_OrderingEquals", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_OrderingEquals", [instance, arguments[0]], typeof(bool)), nameof(Geometry.EqualsTopologically) - => Function("ST_Equals", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Equals", [instance, arguments[0]], typeof(bool)), nameof(Geometry.GetGeometryN) - => Function("ST_GeometryN", new[] { instance, OneBased(arguments[0]) }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_GeometryN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), nameof(Polygon.GetInteriorRingN) - => Function("ST_InteriorRingN", new[] { instance, OneBased(arguments[0]) }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_InteriorRingN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), nameof(LineString.GetPointN) - => Function("ST_PointN", new[] { instance, OneBased(arguments[0]) }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_PointN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Intersection) - => Function("ST_Intersection", new[] { instance, arguments[0] }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_Intersection", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Intersects) - => Function("ST_Intersects", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Intersects", [instance, arguments[0]], typeof(bool)), nameof(Geometry.IsWithinDistance) => Function("ST_DWithin", new[] { instance }.Concat(arguments).ToArray(), typeof(bool)), nameof(Geometry.Normalized) - => Function("ST_Normalize", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_Normalize", [instance], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Overlaps) - => Function("ST_Overlaps", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Overlaps", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Relate) - => Function("ST_Relate", new[] { instance, arguments[0], arguments[1] }, typeof(bool)), + => Function("ST_Relate", [instance, arguments[0], arguments[1]], typeof(bool)), nameof(Geometry.Reverse) - => Function("ST_Reverse", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_Reverse", [instance], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.SymmetricDifference) - => Function("ST_SymDifference", new[] { instance, arguments[0] }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_SymDifference", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.ToBinary) - => Function("ST_AsBinary", new[] { instance }, typeof(byte[])), + => Function("ST_AsBinary", [instance], typeof(byte[])), nameof(Geometry.ToText) - => Function("ST_AsText", new[] { instance }, typeof(string)), + => Function("ST_AsText", [instance], typeof(string)), nameof(Geometry.Touches) - => Function("ST_Touches", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Touches", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Within) - => Function("ST_Within", new[] { instance, arguments[0] }, typeof(bool)), + => Function("ST_Within", [instance, arguments[0]], typeof(bool)), nameof(Geometry.Union) when arguments.Count == 0 - => Function("ST_UnaryUnion", new[] { instance }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_UnaryUnion", [instance], typeof(Geometry), ResultGeometryMapping()), nameof(Geometry.Union) when arguments.Count == 1 - => Function("ST_Union", new[] { instance, arguments[0] }, typeof(Geometry), ResultGeometryMapping()), + => Function("ST_Union", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), _ => null }; - SqlFunctionExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) + SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) => _sqlExpressionFactory.Function( name, arguments, nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs index 266f14da2..4685aa0ce 100644 --- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs +++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs @@ -10,6 +10,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// public sealed class NpgsqlJsonGeometryWktReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonGeometryWktReaderWriter).GetProperty(nameof(Instance))!; + private static readonly WKTReader WktReader = new(); /// @@ -27,5 +29,19 @@ public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager, object /// public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) - => writer.WriteStringValue(value.ToText()); + { + var wkt = value.ToText(); + + // If the SRID is defined, prefix the WKT with it (SRID=4326;POINT(-44.3 60.1)) + // Although this is a PostgreSQL extension, NetTopologySuite supports it (see #3236) + if (value.SRID > 0) + { + wkt = $"SRID={value.SRID};{wkt}"; + } + + writer.WriteStringValue(wkt); + } + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } 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/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs index 732411214..0c070cf11 100644 --- a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs +++ b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs @@ -55,13 +55,8 @@ public virtual void Validate(IDbContextOptions options) } } - private sealed class ExtensionInfo : DbContextOptionsExtensionInfo + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) { - public ExtensionInfo(IDbContextOptionsExtension extension) - : base(extension) - { - } - private new NpgsqlNodaTimeOptionsExtension Extension => (NpgsqlNodaTimeOptionsExtension)base.Extension; 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.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs index 396abd401..32139bda9 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs @@ -25,10 +25,10 @@ public NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin( throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); } - Translators = new IAggregateMethodCallTranslator[] - { + Translators = + [ new NpgsqlNodaTimeAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) - }; + ]; } /// @@ -48,7 +48,7 @@ public NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin( /// public class NpgsqlNodaTimeAggregateMethodTranslator : IAggregateMethodCallTranslator { - private static readonly bool[][] FalseArrays = { Array.Empty(), new[] { false } }; + private static readonly bool[][] FalseArrays = [[], [false]]; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -87,19 +87,19 @@ public NpgsqlNodaTimeAggregateMethodTranslator( return method.Name switch { nameof(NpgsqlNodaTimeDbFunctionsExtensions.Sum) => _sqlExpressionFactory.AggregateFunction( - "sum", new[] { sqlExpression }, source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + "sum", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], returnType: sqlExpression.Type, sqlExpression.TypeMapping), nameof(NpgsqlNodaTimeDbFunctionsExtensions.Average) => _sqlExpressionFactory.AggregateFunction( - "avg", new[] { sqlExpression }, source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + "avg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], returnType: sqlExpression.Type, sqlExpression.TypeMapping), nameof(NpgsqlNodaTimeDbFunctionsExtensions.RangeAgg) => _sqlExpressionFactory.AggregateFunction( - "range_agg", new[] { sqlExpression }, source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + "range_agg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], returnType: method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType)), nameof(NpgsqlNodaTimeDbFunctionsExtensions.RangeIntersectAgg) => _sqlExpressionFactory.AggregateFunction( - "range_intersect_agg", new[] { sqlExpression }, source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + "range_intersect_agg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], returnType: sqlExpression.Type, sqlExpression.TypeMapping), _ => null diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeEvaluatableExpressionFilterPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeEvaluatableExpressionFilterPlugin.cs index 53085d2e4..6e837baf6 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeEvaluatableExpressionFilterPlugin.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeEvaluatableExpressionFilterPlugin.cs @@ -9,7 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; public class NpgsqlNodaTimeEvaluatableExpressionFilterPlugin : IEvaluatableExpressionFilterPlugin { private static readonly MethodInfo GetCurrentInstantMethod = - typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), Array.Empty())!; + typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), [])!; private static readonly MemberInfo SystemClockInstanceMember = typeof(SystemClock).GetMember(nameof(SystemClock.Instance)).FirstOrDefault()!; diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs index 649f9afa0..cda280630 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs @@ -21,10 +21,10 @@ public NpgsqlNodaTimeMemberTranslatorPlugin( IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) { - Translators = new IMemberTranslator[] - { - new NpgsqlNodaTimeMemberTranslator(typeMappingSource, (NpgsqlSqlExpressionFactory)sqlExpressionFactory), - }; + Translators = + [ + new NpgsqlNodaTimeMemberTranslator(typeMappingSource, (NpgsqlSqlExpressionFactory)sqlExpressionFactory) + ]; } /// @@ -100,7 +100,7 @@ public NpgsqlNodaTimeMemberTranslator( _localDateTimeTypeMapping = typeMappingSource.FindMapping(typeof(LocalDateTime))!; } - private static readonly bool[][] TrueArrays = { Array.Empty(), new[] { true }, new[] { true, true } }; + private static readonly bool[][] TrueArrays = [[], [true], [true, true]]; /// public virtual SqlExpression? Translate( @@ -175,7 +175,7 @@ public NpgsqlNodaTimeMemberTranslator( _ => null, }; - SqlBinaryExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) + SqlExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) => _sqlExpressionFactory.Divide(GetDatePartExpressionDouble(instance, "epoch"), _sqlExpressionFactory.Constant(divisor)); } @@ -196,7 +196,7 @@ SqlBinaryExpression TranslateDurationTotalMember(SqlExpression instance, double return _sqlExpressionFactory.Not( _sqlExpressionFactory.Function( "lower_inf", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(bool))); @@ -207,7 +207,7 @@ SqlBinaryExpression TranslateDurationTotalMember(SqlExpression instance, double return _sqlExpressionFactory.Not( _sqlExpressionFactory.Function( "upper_inf", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(bool))); @@ -223,7 +223,7 @@ SqlBinaryExpression TranslateDurationTotalMember(SqlExpression instance, double SqlExpression Lower() => _sqlExpressionFactory.Function( "lower", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(Interval), @@ -232,7 +232,7 @@ SqlExpression Lower() SqlExpression Upper() => _sqlExpressionFactory.Function( "upper", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(Interval), @@ -251,10 +251,13 @@ SqlExpression Upper() if (member == DateInterval_End) { - return + // PostgreSQL creates a result of type 'timestamp without time zone' when subtracting intervals from dates, so add a cast back + // to date. + return _sqlExpressionFactory.Convert( _sqlExpressionFactory.Subtract( Upper(), - _sqlExpressionFactory.Constant(Period.FromDays(1), _periodTypeMapping)); + _sqlExpressionFactory.Constant(Period.FromDays(1), _periodTypeMapping)), typeof(LocalDate), + _typeMappingSource.FindMapping(typeof(LocalDate))); } if (member == DateInterval_Length) @@ -267,7 +270,7 @@ SqlExpression Upper() SqlExpression Lower() => _sqlExpressionFactory.Function( "lower", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(LocalDate), @@ -276,7 +279,7 @@ SqlExpression Lower() SqlExpression Upper() => _sqlExpressionFactory.Function( "upper", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(LocalDate), @@ -301,7 +304,7 @@ SqlExpression Upper() "DayOfWeek" when GetDatePartExpression(instance, "dow", true) is var getValueExpression => _sqlExpressionFactory.Case( getValueExpression, - new[] { new CaseWhenClause(_sqlExpressionFactory.Constant(0), _sqlExpressionFactory.Constant(7)) }, + [new CaseWhenClause(_sqlExpressionFactory.Constant(0), _sqlExpressionFactory.Constant(7))], getValueExpression), // PG allows converting a timestamp directly to date, truncating the time; but given a timestamptz, it performs a time zone @@ -347,7 +350,7 @@ private SqlExpression GetDatePartExpressionDouble( { var result = _sqlExpressionFactory.Function( "date_part", - new[] { _sqlExpressionFactory.Constant(partName), instance }, + [_sqlExpressionFactory.Constant(partName), instance], nullable: true, argumentsPropagateNullability: TrueArrays[2], typeof(double)); @@ -356,7 +359,7 @@ private SqlExpression GetDatePartExpressionDouble( { result = _sqlExpressionFactory.Function( "floor", - new[] { result }, + [result], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(double)); diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs index 7dada739f..2fac8c08e 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs @@ -23,10 +23,10 @@ public NpgsqlNodaTimeMethodCallTranslatorPlugin( IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) { - Translators = new IMethodCallTranslator[] - { - new NpgsqlNodaTimeMethodCallTranslator(typeMappingSource, (NpgsqlSqlExpressionFactory)sqlExpressionFactory), - }; + Translators = + [ + new NpgsqlNodaTimeMethodCallTranslator(typeMappingSource, (NpgsqlSqlExpressionFactory)sqlExpressionFactory) + ]; } /// @@ -56,14 +56,14 @@ public class NpgsqlNodaTimeMethodCallTranslator : IMethodCallTranslator typeof(Instant).GetRuntimeMethod(nameof(Instant.InUtc), Type.EmptyTypes)!; private static readonly MethodInfo Instant_InZone = - typeof(Instant).GetRuntimeMethod(nameof(Instant.InZone), new[] { typeof(DateTimeZone) })!; + typeof(Instant).GetRuntimeMethod(nameof(Instant.InZone), [typeof(DateTimeZone)])!; private static readonly MethodInfo Instant_ToDateTimeUtc = typeof(Instant).GetRuntimeMethod(nameof(Instant.ToDateTimeUtc), Type.EmptyTypes)!; private static readonly MethodInfo Instant_Distance = typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), new[] { typeof(DbFunctions), typeof(Instant), typeof(Instant) })!; + nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(Instant), typeof(Instant)])!; private static readonly MethodInfo ZonedDateTime_ToInstant = typeof(ZonedDateTime).GetRuntimeMethod(nameof(ZonedDateTime.ToInstant), Type.EmptyTypes)!; @@ -71,56 +71,56 @@ public class NpgsqlNodaTimeMethodCallTranslator : IMethodCallTranslator private static readonly MethodInfo ZonedDateTime_Distance = typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), - new[] { typeof(DbFunctions), typeof(ZonedDateTime), typeof(ZonedDateTime) })!; + [typeof(DbFunctions), typeof(ZonedDateTime), typeof(ZonedDateTime)])!; private static readonly MethodInfo LocalDateTime_InZoneLeniently = - typeof(LocalDateTime).GetRuntimeMethod(nameof(LocalDateTime.InZoneLeniently), new[] { typeof(DateTimeZone) })!; + typeof(LocalDateTime).GetRuntimeMethod(nameof(LocalDateTime.InZoneLeniently), [typeof(DateTimeZone)])!; private static readonly MethodInfo LocalDateTime_Distance = typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), - new[] { typeof(DbFunctions), typeof(LocalDateTime), typeof(LocalDateTime) })!; + [typeof(DbFunctions), typeof(LocalDateTime), typeof(LocalDateTime)])!; private static readonly MethodInfo LocalDate_Distance = typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), new[] { typeof(DbFunctions), typeof(LocalDate), typeof(LocalDate) })!; + nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(LocalDate), typeof(LocalDate)])!; - private static readonly MethodInfo Period_FromYears = typeof(Period).GetRuntimeMethod(nameof(Period.FromYears), new[] { typeof(int) })!; + private static readonly MethodInfo Period_FromYears = typeof(Period).GetRuntimeMethod(nameof(Period.FromYears), [typeof(int)])!; private static readonly MethodInfo Period_FromMonths = - typeof(Period).GetRuntimeMethod(nameof(Period.FromMonths), new[] { typeof(int) })!; + typeof(Period).GetRuntimeMethod(nameof(Period.FromMonths), [typeof(int)])!; - private static readonly MethodInfo Period_FromWeeks = typeof(Period).GetRuntimeMethod(nameof(Period.FromWeeks), new[] { typeof(int) })!; - private static readonly MethodInfo Period_FromDays = typeof(Period).GetRuntimeMethod(nameof(Period.FromDays), new[] { typeof(int) })!; + private static readonly MethodInfo Period_FromWeeks = typeof(Period).GetRuntimeMethod(nameof(Period.FromWeeks), [typeof(int)])!; + private static readonly MethodInfo Period_FromDays = typeof(Period).GetRuntimeMethod(nameof(Period.FromDays), [typeof(int)])!; private static readonly MethodInfo Period_FromHours = typeof(Period).GetRuntimeMethod( - nameof(Period.FromHours), new[] { typeof(long) })!; + nameof(Period.FromHours), [typeof(long)])!; private static readonly MethodInfo Period_FromMinutes = - typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), new[] { typeof(long) })!; + typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), [typeof(long)])!; private static readonly MethodInfo Period_FromSeconds = - typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), new[] { typeof(long) })!; + typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), [typeof(long)])!; private static readonly MethodInfo Interval_Contains - = typeof(Interval).GetRuntimeMethod(nameof(Interval.Contains), new[] { typeof(Instant) })!; + = typeof(Interval).GetRuntimeMethod(nameof(Interval.Contains), [typeof(Instant)])!; private static readonly MethodInfo DateInterval_Contains_LocalDate - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), new[] { typeof(LocalDate) })!; + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(LocalDate)])!; private static readonly MethodInfo DateInterval_Contains_DateInterval - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), new[] { typeof(DateInterval) })!; + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(DateInterval)])!; private static readonly MethodInfo DateInterval_Intersection - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Intersection), new[] { typeof(DateInterval) })!; + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Intersection), [typeof(DateInterval)])!; private static readonly MethodInfo DateInterval_Union - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Union), new[] { typeof(DateInterval) })!; + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Union), [typeof(DateInterval)])!; private static readonly MethodInfo IDateTimeZoneProvider_get_Item - = typeof(IDateTimeZoneProvider).GetRuntimeMethod("get_Item", new[] { typeof(string) })!; + = typeof(IDateTimeZoneProvider).GetRuntimeMethod("get_Item", [typeof(string)])!; - private static readonly bool[][] TrueArrays = { Array.Empty(), new[] { true }, new[] { true, true }, }; + private static readonly bool[][] TrueArrays = [[], [true], [true, true]]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -179,17 +179,17 @@ public NpgsqlNodaTimeMethodCallTranslator( ? _sqlExpressionFactory.AtTimeZone( _sqlExpressionFactory.Function( "NOW", - Array.Empty(), + [], nullable: false, - argumentsPropagateNullability: Array.Empty(), + argumentsPropagateNullability: [], method.ReturnType), _sqlExpressionFactory.Constant("UTC"), method.ReturnType) : _sqlExpressionFactory.Function( "NOW", - Array.Empty(), + [], nullable: false, - argumentsPropagateNullability: Array.Empty(), + argumentsPropagateNullability: [], method.ReturnType, _typeMappingSource.FindMapping(typeof(Instant), "timestamp with time zone")); } @@ -336,8 +336,8 @@ public NpgsqlNodaTimeMethodCallTranslator( static PgFunctionExpression IntervalPart(string datePart, SqlExpression parameter) => PgFunctionExpression.CreateWithNamedArguments( "make_interval", - new[] { parameter }, - new[] { datePart }, + [parameter], + [datePart], nullable: true, argumentsPropagateNullability: TrueArrays[1], builtIn: true, diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs b/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs index 11879f214..7c2ee5ef9 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs @@ -9,6 +9,9 @@ private PendingDateTimeZoneProviderExpression() { } + public override Expression Quote() + => throw new UnreachableException("PendingDateTimeZoneProviderExpression is a temporary tree representation and should never be quoted"); + protected override void Print(ExpressionPrinter expressionPrinter) => expressionPrinter.Append("TZDB"); } diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs b/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs index 35c8affee..41739d713 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs @@ -12,6 +12,9 @@ internal PendingZonedDateTimeExpression(SqlExpression operand, SqlExpression tim internal SqlExpression TimeZoneId { get; } + public override Expression Quote() + => throw new UnreachableException("PendingDateTimeZoneProviderExpression is a temporary tree representation and should never be quoted"); + protected override void Print(ExpressionPrinter expressionPrinter) { expressionPrinter.Visit(Operand); diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs index 6ac52f361..3a072e892 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs @@ -13,10 +13,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class DateIntervalRangeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo _constructorWithDates = - typeof(DateInterval).GetConstructor(new[] { typeof(LocalDate), typeof(LocalDate) })!; + typeof(DateInterval).GetConstructor([typeof(LocalDate), typeof(LocalDate)])!; private static readonly ConstructorInfo _localDateConstructor = - typeof(LocalDate).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(LocalDate).GetConstructor([typeof(int), typeof(int), typeof(int)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs index 213f1a488..eea1ef0a5 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs @@ -16,7 +16,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class DateMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo Constructor = - typeof(LocalDate).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(LocalDate).GetConstructor([typeof(int), typeof(int), typeof(int)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -116,6 +116,8 @@ public override Expression GenerateCodeLiteral(object value) private sealed class JsonLocalDateReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonLocalDateReaderWriter).GetProperty(nameof(Instance))!; + public static JsonLocalDateReaderWriter Instance { get; } = new(); public override LocalDate FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -138,5 +140,8 @@ public override LocalDate FromJsonTyped(ref Utf8JsonReaderManager manager, objec public override void ToJsonTyped(Utf8JsonWriter writer, LocalDate value) => writer.WriteStringValue(FormatLocalDate(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs index 6dc203ffd..fea8f021b 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs @@ -52,26 +52,14 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p public override Expression GenerateCodeLiteral(object value) => Expression.Call( Expression.Property(null, typeof(DateTimeZoneProviders).GetProperty(nameof(DateTimeZoneProviders.Tzdb))!), - typeof(IDateTimeZoneProvider).GetMethod(nameof(IDateTimeZoneProvider.GetZoneOrNull), new[] { typeof(string) })!, + typeof(IDateTimeZoneProvider).GetMethod(nameof(IDateTimeZoneProvider.GetZoneOrNull), [typeof(string)])!, Expression.Constant(((DateTimeZone)value).Id)); - private sealed class DateTimeZoneConverter : ValueConverter - { - public DateTimeZoneConverter() - : base( - tz => tz.Id, - id => DateTimeZoneProviders.Tzdb[id]) - { - } - } + private sealed class DateTimeZoneConverter() : ValueConverter( + tz => tz.Id, + id => DateTimeZoneProviders.Tzdb[id]); - private sealed class DateTimeZoneComparer : ValueComparer - { - public DateTimeZoneComparer() - : base( - (tz1, tz2) => tz1 == null ? tz2 == null : tz2 != null && tz1.Id == tz2.Id, - tz => tz.GetHashCode()) - { - } - } + private sealed class DateTimeZoneComparer() : ValueComparer( + (tz1, tz2) => tz1 == null ? tz2 == null : tz2 != null && tz1.Id == tz2.Id, + tz => tz.GetHashCode()); } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs index 0f3385681..bb44e93bb 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs @@ -13,17 +13,17 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// public class DurationIntervalMapping : NpgsqlTypeMapping { - private static readonly MethodInfo FromDays = typeof(Duration).GetRuntimeMethod(nameof(Duration.FromDays), new[] { typeof(int) })!; - private static readonly MethodInfo FromHours = typeof(Duration).GetRuntimeMethod(nameof(Duration.FromHours), new[] { typeof(int) })!; + private static readonly MethodInfo FromDays = typeof(Duration).GetRuntimeMethod(nameof(Duration.FromDays), [typeof(int)])!; + private static readonly MethodInfo FromHours = typeof(Duration).GetRuntimeMethod(nameof(Duration.FromHours), [typeof(int)])!; private static readonly MethodInfo FromMinutes = - typeof(Duration).GetRuntimeMethod(nameof(Duration.FromMinutes), new[] { typeof(long) })!; + typeof(Duration).GetRuntimeMethod(nameof(Duration.FromMinutes), [typeof(long)])!; private static readonly MethodInfo FromSeconds = - typeof(Duration).GetRuntimeMethod(nameof(Duration.FromSeconds), new[] { typeof(long) })!; + typeof(Duration).GetRuntimeMethod(nameof(Duration.FromSeconds), [typeof(long)])!; private static readonly MethodInfo FromMilliseconds = typeof(Duration).GetRuntimeMethod( - nameof(Duration.FromMilliseconds), new[] { typeof(long) })!; + nameof(Duration.FromMilliseconds), [typeof(long)])!; private static readonly PropertyInfo Zero = typeof(Duration).GetProperty(nameof(Duration.Zero))!; @@ -137,6 +137,8 @@ void Compose(Expression toAdd) private sealed class JsonDurationReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonDurationReaderWriter).GetProperty(nameof(Instance))!; + public static JsonDurationReaderWriter Instance { get; } = new(); public override Duration FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -144,5 +146,8 @@ public override Duration FromJsonTyped(ref Utf8JsonReaderManager manager, object public override void ToJsonTyped(Utf8JsonWriter writer, Duration value) => writer.WriteStringValue(NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(value.ToTimeSpan())); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs index a262d0a11..e98372ccb 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs @@ -1,7 +1,8 @@ -// ReSharper disable once CheckNamespace + using System.Text; using NodaTime.Text; +// ReSharper disable once CheckNamespace using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace @@ -16,10 +17,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class IntervalRangeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo _constructor = - typeof(Interval).GetConstructor(new[] { typeof(Instant), typeof(Instant) })!; + typeof(Interval).GetConstructor([typeof(Instant), typeof(Instant)])!; private static readonly ConstructorInfo _constructorWithNulls = - typeof(Interval).GetConstructor(new[] { typeof(Instant?), typeof(Instant?) })!; + typeof(Interval).GetConstructor([typeof(Instant?), typeof(Instant?)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs index d2b6b7116..c31a179ec 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs @@ -88,7 +88,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $""" + "{GenerateLiteralCore(value)}" + """; private string GenerateLiteralCore(object value) { diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs index 7cad0fff4..d3a581c8e 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs @@ -97,30 +97,27 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH // migrations, model snapshots still contain old mappings (Instant mapped to timestamp), and EF Core's model differ // expects type mappings to be found for these. See https://github.com/dotnet/efcore/issues/26168. "timestamp without time zone", LegacyTimestampBehavior - ? new RelationalTypeMapping[] { _legacyTimestampInstant, _timestampLocalDateTime } - : new RelationalTypeMapping[] { _timestampLocalDateTime, _legacyTimestampInstant } + ? [_legacyTimestampInstant, _timestampLocalDateTime] + : [_timestampLocalDateTime, _legacyTimestampInstant] }, { - "timestamp with time zone", - new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } + "timestamp with time zone", [_timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime] }, - { "date", new RelationalTypeMapping[] { _date } }, - { "time without time zone", new RelationalTypeMapping[] { _time } }, - { "time with time zone", new RelationalTypeMapping[] { _timetz } }, - { "interval", new RelationalTypeMapping[] { _periodInterval, _durationInterval } }, + { "date", [_date] }, + { "time without time zone", [_time] }, + { "time with time zone", [_timetz] }, + { "interval", [_periodInterval, _durationInterval] }, { "tsrange", LegacyTimestampBehavior - ? new RelationalTypeMapping[] { _legacyTimestampInstantRange, _timestampLocalDateTimeRange } - : new RelationalTypeMapping[] { _timestampLocalDateTimeRange, _legacyTimestampInstantRange } + ? [_legacyTimestampInstantRange, _timestampLocalDateTimeRange] + : [_timestampLocalDateTimeRange, _legacyTimestampInstantRange] }, { - "tstzrange", - new RelationalTypeMapping[] - { + "tstzrange", [ _intervalRange, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange - } + ] }, - { "daterange", new RelationalTypeMapping[] { _dateIntervalRange, _dateRange } } + { "daterange", [_dateIntervalRange, _dateRange] } }; // Set up aliases diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs index 07ccff8b2..99eba05e1 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs @@ -14,19 +14,19 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// public class PeriodIntervalMapping : NpgsqlTypeMapping { - private static readonly MethodInfo FromYears = typeof(Period).GetRuntimeMethod(nameof(Period.FromYears), new[] { typeof(int) })!; - private static readonly MethodInfo FromMonths = typeof(Period).GetRuntimeMethod(nameof(Period.FromMonths), new[] { typeof(int) })!; - private static readonly MethodInfo FromWeeks = typeof(Period).GetRuntimeMethod(nameof(Period.FromWeeks), new[] { typeof(int) })!; - private static readonly MethodInfo FromDays = typeof(Period).GetRuntimeMethod(nameof(Period.FromDays), new[] { typeof(int) })!; - private static readonly MethodInfo FromHours = typeof(Period).GetRuntimeMethod(nameof(Period.FromHours), new[] { typeof(long) })!; - private static readonly MethodInfo FromMinutes = typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), new[] { typeof(long) })!; - private static readonly MethodInfo FromSeconds = typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), new[] { typeof(long) })!; + private static readonly MethodInfo FromYears = typeof(Period).GetRuntimeMethod(nameof(Period.FromYears), [typeof(int)])!; + private static readonly MethodInfo FromMonths = typeof(Period).GetRuntimeMethod(nameof(Period.FromMonths), [typeof(int)])!; + private static readonly MethodInfo FromWeeks = typeof(Period).GetRuntimeMethod(nameof(Period.FromWeeks), [typeof(int)])!; + private static readonly MethodInfo FromDays = typeof(Period).GetRuntimeMethod(nameof(Period.FromDays), [typeof(int)])!; + private static readonly MethodInfo FromHours = typeof(Period).GetRuntimeMethod(nameof(Period.FromHours), [typeof(long)])!; + private static readonly MethodInfo FromMinutes = typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), [typeof(long)])!; + private static readonly MethodInfo FromSeconds = typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), [typeof(long)])!; private static readonly MethodInfo FromMilliseconds = typeof(Period).GetRuntimeMethod( - nameof(Period.FromMilliseconds), new[] { typeof(long) })!; + nameof(Period.FromMilliseconds), [typeof(long)])!; private static readonly MethodInfo FromNanoseconds = typeof(Period).GetRuntimeMethod( - nameof(Period.FromNanoseconds), new[] { typeof(long) })!; + nameof(Period.FromNanoseconds), [typeof(long)])!; private static readonly PropertyInfo Zero = typeof(Period).GetProperty(nameof(Period.Zero))!; @@ -94,7 +94,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $""" + "{GenerateLiteralCore(value)}" + """; private string GenerateLiteralCore(object value) => PeriodPattern.NormalizingIso.Format((Period)value); @@ -163,6 +165,8 @@ void Compose(Expression toAdd) private sealed class JsonPeriodReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonPeriodReaderWriter).GetProperty(nameof(Instance))!; + public static JsonPeriodReaderWriter Instance { get; } = new(); public override Period FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -170,5 +174,8 @@ public override Period FromJsonTyped(ref Utf8JsonReaderManager manager, object? public override void ToJsonTyped(Utf8JsonWriter writer, Period value) => writer.WriteStringValue(PeriodPattern.NormalizingIso.Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs index b3aab9393..64c8f4c76 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs @@ -16,15 +16,15 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class TimeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo ConstructorWithMinutes = - typeof(LocalTime).GetConstructor(new[] { typeof(int), typeof(int) })!; + typeof(LocalTime).GetConstructor([typeof(int), typeof(int)])!; private static readonly ConstructorInfo ConstructorWithSeconds = - typeof(LocalTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(LocalTime).GetConstructor([typeof(int), typeof(int), typeof(int)])!; private static readonly MethodInfo FromHourMinuteSecondNanosecondMethod = typeof(LocalTime).GetMethod( nameof(LocalTime.FromHourMinuteSecondNanosecond), - new[] { typeof(int), typeof(int), typeof(int), typeof(long) })!; + [typeof(int), typeof(int), typeof(int), typeof(long)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -119,6 +119,8 @@ public override Expression GenerateCodeLiteral(object value) private sealed class JsonLocalTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonLocalTimeReaderWriter).GetProperty(nameof(Instance))!; + public static JsonLocalTimeReaderWriter Instance { get; } = new(); public override LocalTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -126,5 +128,8 @@ public override LocalTime FromJsonTyped(ref Utf8JsonReaderManager manager, objec public override void ToJsonTyped(Utf8JsonWriter writer, LocalTime value) => writer.WriteStringValue(LocalTimePattern.ExtendedIso.Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs index f48ec2566..7ffeb3116 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs @@ -16,24 +16,24 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class TimeTzMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo OffsetTimeConstructor = - typeof(OffsetTime).GetConstructor(new[] { typeof(LocalTime), typeof(Offset) })!; + typeof(OffsetTime).GetConstructor([typeof(LocalTime), typeof(Offset)])!; private static readonly ConstructorInfo LocalTimeConstructorWithMinutes = - typeof(LocalTime).GetConstructor(new[] { typeof(int), typeof(int) })!; + typeof(LocalTime).GetConstructor([typeof(int), typeof(int)])!; private static readonly ConstructorInfo LocalTimeConstructorWithSeconds = - typeof(LocalTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(LocalTime).GetConstructor([typeof(int), typeof(int), typeof(int)])!; private static readonly MethodInfo LocalTimeFromHourMinuteSecondNanosecondMethod = typeof(LocalTime).GetMethod( nameof(LocalTime.FromHourMinuteSecondNanosecond), - new[] { typeof(int), typeof(int), typeof(int), typeof(long) })!; + [typeof(int), typeof(int), typeof(int), typeof(long)])!; private static readonly MethodInfo OffsetFromHoursMethod = - typeof(Offset).GetMethod(nameof(Offset.FromHours), new[] { typeof(int) })!; + typeof(Offset).GetMethod(nameof(Offset.FromHours), [typeof(int)])!; private static readonly MethodInfo OffsetFromSeconds = - typeof(Offset).GetMethod(nameof(Offset.FromSeconds), new[] { typeof(int) })!; + typeof(Offset).GetMethod(nameof(Offset.FromSeconds), [typeof(int)])!; private static readonly OffsetTimePattern Pattern = OffsetTimePattern.CreateWithInvariantCulture("HH':'mm':'ss;FFFFFFo"); @@ -111,7 +111,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{Pattern.Format((OffsetTime)value)}"""; + => $""" + "{Pattern.Format((OffsetTime)value)}" + """; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -150,6 +152,8 @@ public override Expression GenerateCodeLiteral(object value) private sealed class JsonOffsetTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonOffsetTimeReaderWriter).GetProperty(nameof(Instance))!; + public static JsonOffsetTimeReaderWriter Instance { get; } = new(); public override OffsetTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -157,5 +161,8 @@ public override OffsetTime FromJsonTyped(ref Utf8JsonReaderManager manager, obje public override void ToJsonTyped(Utf8JsonWriter writer, OffsetTime value) => writer.WriteStringValue(Pattern.Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs index 63cba6b1c..a3b2b47f5 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs @@ -16,13 +16,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo ConstructorWithMinutes = - typeof(LocalDateTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(int) })!; + typeof(LocalDateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; private static readonly ConstructorInfo ConstructorWithSeconds = - typeof(LocalDateTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int) })!; + typeof(LocalDateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; private static readonly MethodInfo PlusNanosecondsMethod = - typeof(LocalDateTime).GetMethod(nameof(LocalDateTime.PlusNanoseconds), new[] { typeof(long) })!; + typeof(LocalDateTime).GetMethod(nameof(LocalDateTime.PlusNanoseconds), [typeof(long)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -97,7 +97,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{Format((LocalDateTime)value)}"""; + => $""" + "{Format((LocalDateTime)value)}" + """; private static string Format(LocalDateTime localDateTime) { @@ -143,6 +145,8 @@ internal static Expression GenerateCodeLiteral(LocalDateTime dateTime) private sealed class JsonLocalDateTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonLocalDateTimeReaderWriter).GetProperty(nameof(Instance))!; + public static JsonLocalDateTimeReaderWriter Instance { get; } = new(); public override LocalDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -165,5 +169,8 @@ public override LocalDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, o public override void ToJsonTyped(Utf8JsonWriter writer, LocalDateTime value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs index 47bb9381a..bf6f741b2 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs @@ -1,4 +1,3 @@ -using System.Globalization; using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; @@ -88,7 +87,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{Format((Instant)value)}"""; + => $""" + "{Format((Instant)value)}" + """; private static string Format(Instant instant) { @@ -121,10 +122,12 @@ public override Expression GenerateCodeLiteral(object value) => GenerateCodeLiteral((Instant)value); private static readonly MethodInfo _fromUnixTimeTicks - = typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), new[] { typeof(long) })!; + = typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), [typeof(long)])!; private sealed class JsonInstantReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonInstantReaderWriter).GetProperty(nameof(Instance))!; + public static JsonInstantReaderWriter Instance { get; } = new(); public override Instant FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -147,5 +150,8 @@ public override Instant FromJsonTyped(ref Utf8JsonReaderManager manager, object? public override void ToJsonTyped(Utf8JsonWriter writer, Instant value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs index 89ea37df4..8d54d819b 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs @@ -16,13 +16,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; public class TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo Constructor = - typeof(OffsetDateTime).GetConstructor(new[] { typeof(LocalDateTime), typeof(Offset) })!; + typeof(OffsetDateTime).GetConstructor([typeof(LocalDateTime), typeof(Offset)])!; private static readonly MethodInfo OffsetFromHoursMethod = - typeof(Offset).GetMethod(nameof(Offset.FromHours), new[] { typeof(int) })!; + typeof(Offset).GetMethod(nameof(Offset.FromHours), [typeof(int)])!; private static readonly MethodInfo OffsetFromSecondsMethod = - typeof(Offset).GetMethod(nameof(Offset.FromSeconds), new[] { typeof(int) })!; + typeof(Offset).GetMethod(nameof(Offset.FromSeconds), [typeof(int)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -97,7 +97,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{Format((OffsetDateTime)value)}"""; + => $""" + "{Format((OffsetDateTime)value)}" + """; private static string Format(OffsetDateTime offsetDateTime) => OffsetDateTimePattern.ExtendedIso.Format(offsetDateTime); @@ -123,6 +125,8 @@ public override Expression GenerateCodeLiteral(object value) private sealed class JsonOffsetDateTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonOffsetDateTimeReaderWriter).GetProperty(nameof(Instance))!; + public static JsonOffsetDateTimeReaderWriter Instance { get; } = new(); public override OffsetDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -130,5 +134,8 @@ public override OffsetDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, public override void ToJsonTyped(Utf8JsonWriter writer, OffsetDateTime value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs index 999bbe751..8ed3c499f 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs @@ -93,7 +93,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{Pattern.Format((ZonedDateTime)value)}"""; + => $""" + "{Pattern.Format((ZonedDateTime)value)}" + """; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -117,7 +119,7 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(ZonedDateTime).GetConstructor(new[] { typeof(Instant), typeof(DateTimeZone) })!; + typeof(ZonedDateTime).GetConstructor([typeof(Instant), typeof(DateTimeZone)])!; private static readonly MemberInfo TzdbDateTimeZoneSourceDefaultMember = typeof(TzdbDateTimeZoneSource).GetMember(nameof(TzdbDateTimeZoneSource.Default))[0]; @@ -125,10 +127,12 @@ public override Expression GenerateCodeLiteral(object value) private static readonly MethodInfo ForIdMethod = typeof(TzdbDateTimeZoneSource).GetRuntimeMethod( nameof(TzdbDateTimeZoneSource.ForId), - new[] { typeof(string) })!; + [typeof(string)])!; private sealed class JsonZonedDateTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonZonedDateTimeReaderWriter).GetProperty(nameof(Instance))!; + public static JsonZonedDateTimeReaderWriter Instance { get; } = new(); public override ZonedDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) @@ -136,5 +140,8 @@ public override ZonedDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, o public override void ToJsonTyped(Utf8JsonWriter writer, ZonedDateTime value) => writer.WriteStringValue(Pattern.Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index d61efedfb..40d5f17df 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -62,7 +62,7 @@ private static readonly MethodInfo ModelHasAnnotationMethodInfo private static readonly MethodInfo ModelUseKeySequencesMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseKeySequences), new[] { typeof(ModelBuilder), typeof(string), typeof(string) })!; + nameof(NpgsqlModelBuilderExtensions.UseKeySequences), [typeof(ModelBuilder), typeof(string), typeof(string)])!; private static readonly MethodInfo EntityTypeIsUnloggedMethodInfo = typeof(NpgsqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( @@ -91,7 +91,7 @@ private static readonly MethodInfo PropertyHasIdentityOptionsMethodInfo private static readonly MethodInfo PropertyUseSequenceMethodInfo = typeof(NpgsqlPropertyBuilderExtensions).GetRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseSequence), new[] { typeof(PropertyBuilder), typeof(string), typeof(string) })!; + nameof(NpgsqlPropertyBuilderExtensions.UseSequence), [typeof(PropertyBuilder), typeof(string), typeof(string)])!; private static readonly MethodInfo IndexUseCollationMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( @@ -105,10 +105,6 @@ private static readonly MethodInfo IndexHasOperatorsMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasOperators), typeof(IndexBuilder), typeof(string[])); - private static readonly MethodInfo IndexHasSortOrderMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.HasSortOrder), typeof(IndexBuilder), typeof(SortOrder[])); - private static readonly MethodInfo IndexHasNullSortOrderMethodInfo = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlIndexBuilderExtensions.HasNullSortOrder), typeof(IndexBuilder), typeof(NullSortOrder[])); @@ -352,9 +348,9 @@ public override IReadOnlyList GenerateFluentApiCalls( onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, (name, schema) switch { - (null, null) => Array.Empty(), - (_, null) => new object[] { name }, - _ => new object?[] { name!, schema } + (null, null) => [], + (_, null) => [name], + _ => [name!, schema] }); } @@ -368,9 +364,9 @@ public override IReadOnlyList GenerateFluentApiCalls( onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, (name: nameOrSuffix, schema) switch { - (null, null) => Array.Empty(), - (_, null) => new object[] { nameOrSuffix }, - _ => new object[] { nameOrSuffix!, schema } + (null, null) => [], + (_, null) => [nameOrSuffix], + _ => [nameOrSuffix!, schema] }); } case NpgsqlValueGenerationStrategy.None: @@ -423,8 +419,6 @@ public override IReadOnlyList GenerateFluentApiCalls( => new MethodCallCodeFragment(IndexHasMethodMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexOperators => new MethodCallCodeFragment(IndexHasOperatorsMethodInfo, annotation.Value), - NpgsqlAnnotationNames.IndexSortOrder - => new MethodCallCodeFragment(IndexHasSortOrderMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexNullSortOrder => new MethodCallCodeFragment(IndexHasNullSortOrderMethodInfo, annotation.Value), NpgsqlAnnotationNames.IndexInclude diff --git a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs index 9d26dfce5..e484cb56e 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs @@ -14,8 +14,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// #pragma warning disable EF1001 // Internal EF Core API usage. -public class NpgsqlCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator +public class NpgsqlCSharpRuntimeAnnotationCodeGenerator + : RelationalCSharpRuntimeAnnotationCodeGenerator, ICSharpRuntimeAnnotationCodeGenerator { + private int _typeMappingNestingCount; + /// /// 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 @@ -42,13 +45,37 @@ public override bool Create( ValueComparer? keyValueComparer = null, ValueComparer? providerValueComparer = null) { - var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + _typeMappingNestingCount++; + try + { + var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + AddNpgsqlTypeMappingTweaks(typeMapping, parameters); + return result; + } + finally + { + _typeMappingNestingCount--; + } + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual void AddNpgsqlTypeMappingTweaks( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { var mainBuilder = parameters.MainBuilder; var npgsqlDbTypeBasedDefaultInstance = typeMapping switch { NpgsqlStringTypeMapping => NpgsqlStringTypeMapping.Default, + NpgsqlUIntTypeMapping => NpgsqlUIntTypeMapping.Default, NpgsqlULongTypeMapping => NpgsqlULongTypeMapping.Default, // NpgsqlMultirangeTypeMapping => NpgsqlMultirangeTypeMapping.Default, _ => (INpgsqlTypeMapping?)null @@ -56,6 +83,8 @@ public override bool Create( if (npgsqlDbTypeBasedDefaultInstance is not null) { + CheckElementTypeMapping(); + var npgsqlDbType = ((INpgsqlTypeMapping)typeMapping).NpgsqlDbType; if (npgsqlDbType != npgsqlDbTypeBasedDefaultInstance.NpgsqlDbType) @@ -81,18 +110,47 @@ public override bool Create( switch (typeMapping) { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete case NpgsqlEnumTypeMapping enumTypeMapping: - if (enumTypeMapping.NameTranslator != NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator) + CheckElementTypeMapping(); + + 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: { + CheckElementTypeMapping(); + var defaultInstance = NpgsqlRangeTypeMapping.Default; var npgsqlDbTypeDifferent = rangeTypeMapping.NpgsqlDbType != defaultInstance.NpgsqlDbType; @@ -126,10 +184,16 @@ public override bool Create( break; } - } - return result; + void CheckElementTypeMapping() + { + if (_typeMappingNestingCount > 1) + { + throw new NotSupportedException( + $"Non-default Npgsql type mappings ('{typeMapping.GetType().Name}' with store type '{(typeMapping as RelationalTypeMapping)?.StoreType}') aren't currently supported as element type mappings, see https://github.com/npgsql/efcore.pg/issues/3366."); + } + } } /// diff --git a/src/EFCore.PG/EFCore.PG.csproj b/src/EFCore.PG/EFCore.PG.csproj index 0ab1ace12..c344ed2e2 100644 --- a/src/EFCore.PG/EFCore.PG.csproj +++ b/src/EFCore.PG/EFCore.PG.csproj @@ -9,13 +9,13 @@ PostgreSQL/Npgsql provider for Entity Framework Core. npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql README.md + EF9100 - diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs index 0d78f939f..52d134bf4 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs @@ -294,45 +294,4 @@ public static EntityTypeBuilder UseCockroachDbInterleaveInParent - /// Configures using the auto-updating system column xmin as the optimistic concurrency token. - /// - /// The builder for the entity type being configured. - /// The same builder instance so that multiple calls can be chained. - /// - /// See Concurrency tokens - /// for more information on using optimistic concurrency in PostgreSQL. - /// - [Obsolete("Use EF Core's standard IsRowVersion() or [Timestamp], see https://learn.microsoft.com/ef/core/saving/concurrency")] - public static EntityTypeBuilder UseXminAsConcurrencyToken( - this EntityTypeBuilder entityTypeBuilder) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - entityTypeBuilder.Property("xmin") - .HasColumnType("xid") - .ValueGeneratedOnAddOrUpdate() - .IsConcurrencyToken(); - - return entityTypeBuilder; - } - - /// - /// Configures using the auto-updating system column xmin as the optimistic concurrency token. - /// - /// - /// See http://www.npgsql.org/efcore/miscellaneous.html#optimistic-concurrency-and-concurrency-tokens - /// - /// The builder for the entity type being configured. - /// The same builder instance so that multiple calls can be chained. - [Obsolete("Use EF Core's standard IsRowVersion() or [Timestamp], see https://learn.microsoft.com/ef/core/saving/concurrency")] - public static EntityTypeBuilder UseXminAsConcurrencyToken( - this EntityTypeBuilder entityTypeBuilder) - where TEntity : class - => (EntityTypeBuilder)UseXminAsConcurrencyToken((EntityTypeBuilder)entityTypeBuilder); - - #endregion Obsolete } diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs index cbe0ea1fa..741bff72a 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs @@ -847,281 +847,4 @@ public static bool CanSetStorageParameter( } #endregion Storage parameters - - #region Sort order (legacy) - - /// - /// The PostgreSQL index sort ordering to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// The sort order to use for each column. - /// A builder to further configure the index. - [Obsolete("Use IsDescending instead")] - public static IndexBuilder HasSortOrder( - this IndexBuilder indexBuilder, - params SortOrder[]? values) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(values, nameof(values)); - - var isDescending = new bool[indexBuilder.Metadata.Properties.Count]; - - for (var i = 0; i < isDescending.Length; i++) - { - isDescending[i] = values?.Length > i && values[i] == SortOrder.Descending; - } - - indexBuilder.IsDescending(isDescending); - - return indexBuilder; - } - - /// - /// The PostgreSQL index sort ordering to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// The sort order to use for each column. - /// A builder to further configure the index. - [Obsolete("Use IsDescending instead")] - public static IndexBuilder HasSortOrder( - this IndexBuilder indexBuilder, - params SortOrder[]? values) - => (IndexBuilder)HasSortOrder((IndexBuilder)indexBuilder, values); - - /// - /// The PostgreSQL index sort ordering to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// Indicates whether the configuration was specified using a data annotation. - /// The sort order to use for each column. - /// A builder to further configure the index. - [Obsolete("Use IsDescending instead")] - public static IConventionIndexBuilder? HasSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - if (indexBuilder.CanSetSortOrder(values, fromDataAnnotation)) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(values, nameof(values)); - - var isDescending = new bool[indexBuilder.Metadata.Properties.Count]; - - for (var i = 0; i < isDescending.Length; i++) - { - isDescending[i] = values?.Count > i && values[i] == SortOrder.Descending; - } - - indexBuilder.IsDescending(isDescending); - - return indexBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the PostgreSQL index sort ordering can be set. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// The sort order to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - [Obsolete("Use IsDescending instead")] - public static bool CanSetSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.IndexSortOrder, values, fromDataAnnotation); - } - - #endregion Sort order (obsolete) - - #region Obsolete - - /// - /// The PostgreSQL index collation to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - /// The builder for the index being configured. - /// The sort options to use for each column. - /// A builder to further configure the index. - [Obsolete("Use UseCollation")] - public static IndexBuilder HasCollation( - this IndexBuilder indexBuilder, - params string[]? values) - => UseCollation(indexBuilder, values); - - /// - /// The PostgreSQL index collation to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - /// The builder for the index being configured. - /// The sort options to use for each column. - /// A builder to further configure the index. - [Obsolete("Use UseCollation")] - public static IndexBuilder HasCollation( - this IndexBuilder indexBuilder, - params string[]? values) - => UseCollation(indexBuilder, values); - - /// - /// The PostgreSQL index collation to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - /// The builder for the index being configured. - /// The sort options to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - [Obsolete("Use UseCollation")] - public static IConventionIndexBuilder? HasCollation( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - => UseCollation(indexBuilder, values, fromDataAnnotation); - - /// - /// Returns a value indicating whether the PostgreSQL index collation can be set. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - /// The builder for the index being configured. - /// The sort options to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - [Obsolete("Use CanSetHasCollation")] - public static bool CanSetHasCollation( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - => CanSetCollation(indexBuilder, values, fromDataAnnotation); - - /// - /// The PostgreSQL index method to be used. Null selects the default (currently btree). - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - /// The builder for the index being configured. - /// The name of the index. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured with the method - [Obsolete("Use CanSetMethod")] - public static bool CanSetHasMethod( - this IConventionIndexBuilder indexBuilder, - string? method, - bool fromDataAnnotation = false) - => CanSetMethod(indexBuilder, method, fromDataAnnotation); - - /// - /// Returns a value indicating whether the PostgreSQL index operators can be set. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - /// The builder for the index being configured. - /// The operators to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured with the method. - [Obsolete("Use CanSetOperators")] - public static bool CanSetHasOperators( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? operators, - bool fromDataAnnotation) - => CanSetOperators(indexBuilder, operators, fromDataAnnotation); - - /// - /// Returns a value indicating whether the index can be configured as a full-text tsvector expression index. - /// - /// The builder for the index being configured. - /// - /// - /// The text search configuration for this generated tsvector property, or null if this is not a - /// generated tsvector property. - /// - /// - /// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information. - /// - /// - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured as a full-text tsvector expression index. - [Obsolete("Use CanSetIsTsVectorExpressionIndex")] - public static bool CanSetToTsVector( - this IConventionIndexBuilder indexBuilder, - string? config, - bool fromDataAnnotation = false) - => CanSetIsTsVectorExpressionIndex(indexBuilder, config, fromDataAnnotation); - - /// - /// Returns a value indicating whether the PostgreSQL index sort ordering can be set. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// The sort order to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - [Obsolete("Use CanSetSortOrder")] - public static bool CanSetHasSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - => CanSetSortOrder(indexBuilder, values, fromDataAnnotation); - - /// - /// Returns a value indicating whether the PostgreSQL index null sort ordering can be set. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - /// The builder for the index being configured. - /// The sort order to use for each column. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - [Obsolete("Use CanSetNullSortOrder")] - public static bool CanSetHasNullSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - => CanSetNullSortOrder(indexBuilder, values, fromDataAnnotation); - - /// - /// Returns a value indicating whether the given include properties can be set. - /// - /// The builder for the index being configured. - /// An array of property names to be used in 'include' clause. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given include properties can be set. - [Obsolete("Use CanSetIncludeProperties")] - public static bool CanSetInclude( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? propertyNames, - bool fromDataAnnotation = false) - => CanSetIncludeProperties(indexBuilder, propertyNames, fromDataAnnotation); - - #endregion Obsolete } diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 6cfd490c1..6650eb824 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. /// @@ -713,94 +768,6 @@ public static ModelBuilder HasCollation( #endregion Collation management - #region Default column collation - - /// - /// Configures the default collation for all columns in the database. This causes EF Core to specify an explicit - /// collation when creating each column (unless overridden). - /// - /// - ///

- /// An alternative is to specify a database collation via - /// , - /// which will specify the query on CREATE DATABASE instead of for each and every column. However, - /// PostgreSQL support is limited for the collations that can be specific via this mechanism; ICU collations - - /// which include all case-insensitive collations - are currently unsupported. - ///

- ///

- /// For more information, see https://www.postgresql.org/docs/current/collation.html. - ///

- ///
- /// The model builder. - /// The collation. - /// A builder to further configure the property. - [Obsolete("Use EF Core's standard model bulk configuration API")] - public static ModelBuilder UseDefaultColumnCollation(this ModelBuilder modelBuilder, string? collation) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NullButNotEmpty(collation, nameof(collation)); - - modelBuilder.Model.SetDefaultColumnCollation(collation); - - return modelBuilder; - } - - /// - /// Configures the default collation for all columns in the database. This causes EF Core to specify an explicit - /// collation when creating each column (unless overridden). - /// - /// - ///

- /// An alternative is to specify a database collation via - /// , - /// which will specify the query on CREATE DATABASE instead of for each and every column. However, - /// PostgreSQL support is limited for the collations that can be specific via this mechanism; ICU collations - - /// which include all case-insensitive collations - are currently unsupported. - ///

- ///

- /// For more information, see https://www.postgresql.org/docs/current/collation.html. - ///

- ///
- /// The model builder. - /// The collation. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the property. - [Obsolete("Use EF Core's standard model bulk configuration API")] - public static IConventionModelBuilder? UseDefaultColumnCollation( - this IConventionModelBuilder modelBuilder, - string? collation, - bool fromDataAnnotation = false) - { - if (modelBuilder.CanSetDefaultColumnCollation(collation, fromDataAnnotation)) - { - modelBuilder.Metadata.SetDefaultColumnCollation(collation); - return modelBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the given value can be set as the default column collation. - /// - /// The model builder. - /// The collation. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given value can be set as the collation. - [Obsolete("Use EF Core's standard model bulk configuration API")] - public static bool CanSetDefaultColumnCollation( - this IConventionModelBuilder modelBuilder, - string? collation, - bool fromDataAnnotation = false) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - - return modelBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.DefaultColumnCollation, collation, fromDataAnnotation); - } - - #endregion Default column collation - #region Helpers // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeMapping/TypeMapperBase.cs#L132-L138 diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs index 6c4362b7b..2698800d3 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs @@ -425,6 +425,26 @@ public static PropertyBuilder UseIdentityColumn( return null; } + /// + /// Returns a value indicating whether the given value can be set as the value generation strategy for a particular table. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + NpgsqlValueGenerationStrategy? valueGenerationStrategy, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => propertyBuilder.Metadata.FindOverrides(storeObject)?.Builder + .CanSetAnnotation( + NpgsqlAnnotationNames.ValueGenerationStrategy, + valueGenerationStrategy, + fromDataAnnotation) + ?? true; + /// /// Returns a value indicating whether the given value can be set as the value generation strategy. /// @@ -436,14 +456,8 @@ public static bool CanSetValueGenerationStrategy( this IConventionPropertyBuilder propertyBuilder, NpgsqlValueGenerationStrategy? valueGenerationStrategy, bool fromDataAnnotation = false) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - return (valueGenerationStrategy is null - || NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(propertyBuilder.Metadata)) - && propertyBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); - } + => propertyBuilder.CanSetAnnotation( + NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); #endregion General value generation strategy diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs index 9488c8ec8..868faa7e2 100644 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs @@ -141,4 +141,24 @@ public static int Distance(this DbFunctions _, DateOnly a, DateOnly b) /// public static TimeSpan Distance(this DbFunctions _, DateTime a, DateTime b) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Converts string to date according to the given format. + /// + /// The instance. + /// The string to be converted. + /// The format of the input date. + /// + public static DateOnly ToDate(this DbFunctions _, string value, string format) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToDate))); + + /// + /// Converts string to time stamp according to the given format. + /// + /// The instance. + /// The string to be converted + /// The format of the input date + /// + public static DateTime ToTimestamp(this DbFunctions _, string value, string format) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTimestamp))); } diff --git a/src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs b/src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs index d1d72da0b..1007adc98 100644 --- a/src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs +++ b/src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs @@ -3,8 +3,6 @@ // ReSharper disable once CheckNamespace namespace System.Linq.Expressions; -#nullable enable - [DebuggerStepThrough] internal static class ExpressionVisitorExtensions { @@ -16,12 +14,13 @@ internal static class ExpressionVisitorExtensions /// /// The modified expression list, if any of the elements were modified; otherwise, returns the original expression list. /// - public static IReadOnlyList Visit(this ExpressionVisitor visitor, IReadOnlyList nodes) + public static IReadOnlyList Visit(this ExpressionVisitor visitor, IReadOnlyList nodes) + where T : Expression { - Expression[]? newNodes = null; + T[]? newNodes = null; for (int i = 0, n = nodes.Count; i < n; i++) { - var node = visitor.Visit(nodes[i]); + var node = (T)visitor.Visit(nodes[i]); if (newNodes is not null) { @@ -29,7 +28,7 @@ public static IReadOnlyList Visit(this ExpressionVisitor visitor, IR } else if (!ReferenceEquals(node, nodes[i])) { - newNodes = new Expression[n]; + newNodes = new T[n]; for (var j = 0; j < i; j++) { newNodes[j] = nodes[j]; diff --git a/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs b/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs new file mode 100644 index 000000000..e234ac48c --- /dev/null +++ b/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs @@ -0,0 +1,158 @@ +using System.Diagnostics.CodeAnalysis; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Extensions.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 static class NpgsqlShapedQueryExpressionExtensions +{ + /// + /// If the given wraps an array-returning expression without any additional clauses (e.g. filter, + /// ordering...), returns that expression. + /// + /// + /// 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 bool TryExtractArray( + this ShapedQueryExpression source, + [NotNullWhen(true)] out SqlExpression? array, + bool ignoreOrderings = false, + bool ignorePredicate = false) + => TryExtractArray(source, out array, out _, ignoreOrderings, ignorePredicate); + + /// + /// If the given wraps an array-returning expression without any additional clauses (e.g. filter, + /// ordering...), returns that expression. + /// + /// + /// 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 bool TryExtractArray( + this ShapedQueryExpression source, + [NotNullWhen(true)] out SqlExpression? array, + [NotNullWhen(true)] out ColumnExpression? projectedColumn, + bool ignoreOrderings = false, + bool ignorePredicate = false) + { + if (source.QueryExpression is SelectExpression + { + Tables: [PgUnnestExpression { Array: var a } unnest], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null + } select + && (ignorePredicate || select.Predicate is null) + // We can only apply the indexing if the JSON array is ordered by its natural ordered, i.e. by the "ordinality" column that + // we created in TranslatePrimitiveCollection. For example, if another ordering has been applied (e.g. by the array elements + // themselves), we can no longer simply index into the original array. + && (ignoreOrderings + || select.Orderings is [] + || (select.Orderings is [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }] + && orderingTableAlias == unnest.Alias)) + && IsPostgresArray(a) + && TryGetProjectedColumn(source, out var column)) + { + array = a; + projectedColumn = column; + return true; + } + + array = null; + projectedColumn = null; + return false; + } + + /// + /// If the given wraps a without any additional clauses (e.g. filter, + /// ordering...), converts that to a and returns that. + /// + /// + /// 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 bool TryConvertValuesToArray( + this ShapedQueryExpression source, + [NotNullWhen(true)] out SqlExpression? array, + bool ignoreOrderings = false, + bool ignorePredicate = false) + { + if (source.QueryExpression is SelectExpression + { + Tables: [ValuesExpression { ColumnNames: ["_ord", "Value"], RowValues.Count: > 0 } valuesExpression], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null + } select + && (ignorePredicate || select.Predicate is null) + && (ignoreOrderings || select.Orderings is [])) + { + var elements = new SqlExpression[valuesExpression.RowValues.Count]; + + for (var i = 0; i < elements.Length; i++) + { + // Skip the first column (_ord) and copy the second (Value) + elements[i] = valuesExpression.RowValues[i].Values[1]; + } + + array = new PgNewArrayExpression(elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null); + return true; + } + + array = null; + return false; + } + + /// + /// Checks whether the given expression maps to a PostgreSQL array, as opposed to a multirange type. + /// + private static bool IsPostgresArray(SqlExpression expression) + => expression switch + { + { TypeMapping: NpgsqlArrayTypeMapping } => true, + { TypeMapping: NpgsqlMultirangeTypeMapping } => false, + { Type: var type } when type.IsMultirange() => false, + _ => true + }; + + private static bool TryGetProjectedColumn( + ShapedQueryExpression shapedQueryExpression, + [NotNullWhen(true)] out ColumnExpression? projectedColumn) + { + var shaperExpression = shapedQueryExpression.ShaperExpression; + if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression + && unaryExpression.Operand.Type.IsNullableType() + && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) + { + shaperExpression = unaryExpression.Operand; + } + + if (shaperExpression is ProjectionBindingExpression projectionBindingExpression + && shapedQueryExpression.QueryExpression is SelectExpression selectExpression + && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression c) + { + projectedColumn = c; + return true; + } + + projectedColumn = null; + return false; + } +} diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs index fe7154f9b..e3e86d6bb 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs @@ -479,56 +479,4 @@ public static object SetStorageParameter( } #endregion Storage parameters - - #region Sort order (legacy) - - /// - /// Returns the column sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - [Obsolete("Use IsDescending instead")] - public static IReadOnlyList? GetSortOrder(this IReadOnlyIndex index) - => (IReadOnlyList?)index[NpgsqlAnnotationNames.IndexSortOrder]; - - /// - /// Sets the column sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - [Obsolete("Use IsDescending instead")] - public static void SetSortOrder(this IMutableIndex index, IReadOnlyList? sortOrder) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexSortOrder, sortOrder); - - /// - /// Sets the column sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - [Obsolete("Use IsDescending instead")] - public static IReadOnlyList? SetSortOrder( - this IConventionIndex index, - IReadOnlyList? sortOrder, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(sortOrder, nameof(sortOrder)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexSortOrder, sortOrder, fromDataAnnotation); - - return sortOrder; - } - - /// - /// Returns the for the index sort orders. - /// - /// The index. - /// The for the index sort orders. - [Obsolete("Use IsDescending instead")] - public static ConfigurationSource? GetSortOrderConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexSortOrder)?.GetConfigurationSource(); - - #endregion Sort order (legacy) } diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs index 018c77f5f..a69ccf8ac 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 @@ -503,74 +516,4 @@ public static IReadOnlyList GetCollations(this IReadOnlyModel => PostgresCollation.GetCollations(model).ToArray(); #endregion Collation management - - #region Default column collation - - /// - /// Gets the default collation for all columns in the database, or if none is defined. - /// This causes EF Core to specify an explicit collation when creating all column, unless one is overridden - /// on a column. - /// - /// - ///

- /// See for another approach to defining a - /// database-wide collation. - ///

- ///

- /// For more information, see https://www.postgresql.org/docs/current/collation.html. - ///

- ///
- [Obsolete("Use EF Core's standard model bulk configuration API")] - public static string? GetDefaultColumnCollation(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.DefaultColumnCollation]; - - /// - /// Sets the default collation for all columns in the database, or null if none is defined. - /// This causes EF Core to specify an explicit collation when creating all column, unless one is overridden - /// on a column. - /// - /// - ///

- /// See for another approach to defining a - /// database-wide collation. - ///

- ///

- /// For more information, see https://www.postgresql.org/docs/current/collation.html. - ///

- ///
- [Obsolete("Use EF Core's standard model bulk configuration API")] - public static void SetDefaultColumnCollation(this IMutableModel model, string? collation) - => model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.DefaultColumnCollation, collation); - - /// - /// Sets the default collation for all columns in the database, or null if none is defined. - /// This causes EF Core to specify an explicit collation when creating all column, unless one is overridden - /// on a column. - /// - /// - ///

- /// See - /// for another approach to defining a database-wide collation. - ///

- ///

- /// For more information, see https://www.postgresql.org/docs/current/collation.html. - ///

- ///
- [Obsolete("Use EF Core's standard model bulk configuration API")] - public static string? SetDefaultColumnCollation(this IConventionModel model, string? collation, bool fromDataAnnotation = false) - { - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.DefaultColumnCollation, collation, fromDataAnnotation); - return collation; - } - - /// - /// Returns the for the default column collation. - /// - /// The model. - /// The for the default column collation. - [Obsolete("Use EF Core's standard model bulk configuration API")] - public static ConfigurationSource? GetDefaultColumnCollationConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.DefaultColumnCollation)?.GetConfigurationSource(); - - #endregion Default column collation } diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs index da331f59f..8e4869b7b 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs @@ -1,4 +1,3 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; @@ -589,11 +588,7 @@ private static NpgsqlValueGenerationStrategy GetDefaultValueGenerationStrategy( public static void SetValueGenerationStrategy( this IMutableProperty property, NpgsqlValueGenerationStrategy? value) - { - CheckValueGenerationStrategy(property, value); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); - } + => property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); /// /// Sets the to use for the property. @@ -605,13 +600,8 @@ public static void SetValueGenerationStrategy( this IConventionProperty property, NpgsqlValueGenerationStrategy? value, bool fromDataAnnotation = false) - { - CheckValueGenerationStrategy(property, value); - - return (NpgsqlValueGenerationStrategy?)property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation) - ?.Value; - } + => (NpgsqlValueGenerationStrategy?)property.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; /// /// Sets the to use for the property for a particular table. @@ -650,11 +640,7 @@ public static void SetValueGenerationStrategy( public static void SetValueGenerationStrategy( this IMutableRelationalPropertyOverrides overrides, NpgsqlValueGenerationStrategy? value) - { - CheckValueGenerationStrategy(overrides.Property, value); - - overrides.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); - } + => overrides.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); /// /// Sets the to use for the property for a particular table. @@ -667,37 +653,8 @@ public static void SetValueGenerationStrategy( this IConventionRelationalPropertyOverrides overrides, NpgsqlValueGenerationStrategy? value, bool fromDataAnnotation = false) - { - CheckValueGenerationStrategy(overrides.Property, value); - - return (NpgsqlValueGenerationStrategy?)overrides.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation) - ?.Value; - } - - private static void CheckValueGenerationStrategy(IReadOnlyProperty property, NpgsqlValueGenerationStrategy? value) - { - if (value is not null) - { - var propertyType = property.ClrType; - - if ((value is NpgsqlValueGenerationStrategy.IdentityAlwaysColumn or NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - && !IsCompatibleWithValueGeneration(property)) - { - throw new ArgumentException( - NpgsqlStrings.IdentityBadType( - property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); - } - - if (value is NpgsqlValueGenerationStrategy.SerialColumn or NpgsqlValueGenerationStrategy.SequenceHiLo - && !IsCompatibleWithValueGeneration(property)) - { - throw new ArgumentException( - NpgsqlStrings.SequenceBadType( - property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); - } - } - } + => (NpgsqlValueGenerationStrategy?)overrides.SetOrRemoveAnnotation( + NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; /// /// Returns the for the . @@ -1185,23 +1142,6 @@ public static void SetTsVectorProperties( #endregion Generated tsvector column - #region Collation - - /// - /// Returns the collation to be used for the column - including the PostgreSQL-specific default column - /// collation defined at the model level (see - /// ). - /// - /// The property. - /// The collation for the column this property is mapped to. - [Obsolete("Use EF Core's standard model bulk configuration API")] - public static string? GetDefaultCollation(this IReadOnlyProperty property) - => property.FindTypeMapping() is StringTypeMapping - ? property.DeclaringType.Model.GetDefaultColumnCollation() - : null; - - #endregion Collation - #region Compression method /// diff --git a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs index 20848999b..35ac4667b 100644 --- a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs @@ -44,30 +44,4 @@ public static MigrationBuilder EnsurePostgresExtension( return builder; } - - /// - /// 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. - /// - [Obsolete("Use EnsurePostgresExtension instead")] - public static MigrationBuilder CreatePostgresExtension( - this MigrationBuilder builder, - string name, - string? schema = null, - string? version = null) - => EnsurePostgresExtension(builder, name, schema, version); - - /// - /// 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. - /// - [Obsolete("This no longer does anything and should be removed.")] - public static MigrationBuilder DropPostgresExtension( - this MigrationBuilder builder, - string name) - => builder; } diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index 4c3e8a1cf..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()) @@ -114,7 +114,6 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() - .TryAdd() .TryAdd() .TryAdd() .TryAdd() @@ -125,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..b02ad9b78 --- /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 ?? nameTranslator.TranslateTypeName(clrType.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..0f08a215f 100644 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs @@ -1,6 +1,4 @@ -using System.Data.Common; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; /// /// Represents options for Npgsql that can only be set at the singleton level. @@ -28,17 +26,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/NpgsqlModelValidator.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs index 14f8dcbf2..6942a4f1e 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs @@ -103,6 +103,55 @@ protected override void ValidateValueGeneration( } } + /// + protected override void ValidateTypeMappings( + IModel model, + IDiagnosticsLogger logger) + { + base.ValidateTypeMappings(model, logger); + + foreach (var entityType in model.GetEntityTypes()) + { + foreach (var property in entityType.GetFlattenedDeclaredProperties()) + { + var strategy = property.GetValueGenerationStrategy(); + var propertyType = property.ClrType; + + switch (strategy) + { + case NpgsqlValueGenerationStrategy.None: + break; + + case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: + case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: + if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + NpgsqlStrings.IdentityBadType( + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); + } + + break; + + case NpgsqlValueGenerationStrategy.SequenceHiLo: + case NpgsqlValueGenerationStrategy.Sequence: + case NpgsqlValueGenerationStrategy.SerialColumn: + if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + NpgsqlStrings.SequenceBadType( + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); + } + + break; + + default: + throw new UnreachableException(); + } + } + } + } + /// /// 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/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 7ba3f5fec..269a66fea 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -12,6 +12,7 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension { private DbContextOptionsExtensionInfo? _info; private readonly List _userRangeDefinitions; + private readonly List _enumDefinitions; private Version? _postgresVersion; @@ -35,6 +36,11 @@ public virtual Version PostgresVersion public virtual bool IsPostgresVersionSet => _postgresVersion is not null; + /// + /// A lambda to configure Npgsql options on . + /// + public virtual Action? DataSourceBuilderAction { get; private set; } + /// /// The , or if a connection string or was used /// instead of a . @@ -57,6 +63,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 . /// @@ -84,7 +96,8 @@ public virtual IReadOnlyList UserRangeDefinitions /// public NpgsqlOptionsExtension() { - _userRangeDefinitions = new List(); + _userRangeDefinitions = []; + _enumDefinitions = []; } // NB: When adding new options, make sure to update the copy ctor below. @@ -96,10 +109,12 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom) : base(copyFrom) { DataSource = copyFrom.DataSource; + DataSourceBuilderAction = copyFrom.DataSourceBuilderAction; AdminDatabase = copyFrom.AdminDatabase; _postgresVersion = copyFrom._postgresVersion; UseRedshift = copyFrom.UseRedshift; - _userRangeDefinitions = new List(copyFrom._userRangeDefinitions); + _userRangeDefinitions = [..copyFrom._userRangeDefinitions]; + _enumDefinitions = [..copyFrom._enumDefinitions]; ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback; RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback; ProvidePasswordCallback = copyFrom.ProvidePasswordCallback; @@ -117,6 +132,21 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom) public override int? MinBatchSize => base.MinBatchSize ?? 2; + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// A lambda to configure Npgsql options on . + /// A new instance with the option changed. + public virtual NpgsqlOptionsExtension WithDataSourceConfiguration(Action dataSourceBuilderAction) + { + var clone = (NpgsqlOptionsExtension)Clone(); + + clone.DataSourceBuilderAction = dataSourceBuilderAction; + + return clone; + } + /// /// Creates a new instance with all options the same as for this instance, but with the given option changed. /// It is unusual to call this method directly. Instead use . @@ -180,6 +210,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. /// @@ -333,16 +388,11 @@ public override void ApplyServices(IServiceCollection services) public override DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); - private sealed class ExtensionInfo : RelationalExtensionInfo + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : RelationalExtensionInfo(extension) { private int? _serviceProviderHash; private string? _logFragment; - public ExtensionInfo(IDbContextOptionsExtension extension) - : base(extension) - { - } - private new NpgsqlOptionsExtension Extension => (NpgsqlOptionsExtension)base.Extension; @@ -402,12 +452,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) { @@ -458,8 +508,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; @@ -487,6 +537,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[ @@ -501,50 +561,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..ad07f951d 100644 --- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs +++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs @@ -18,6 +18,19 @@ public NpgsqlDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) { } + /// + /// Configures lower-level Npgsql options at the ADO.NET driver level. + /// + /// A lambda to configure Npgsql options on . + /// + /// Changes made by are untracked; When using , EF Core + /// will by default resolve the same internally, disregarding differing configuration across calls + /// to . Either make sure that always sets the same + /// configuration, or pass externally-provided, pre-configured data source instances when configuring the provider. + /// + public virtual NpgsqlDbContextOptionsBuilder ConfigureDataSource(Action dataSourceBuilderAction) + => WithOption(e => e.WithDataSourceConfiguration(dataSourceBuilderAction)); + /// /// Connect to this database for administrative operations (creating/dropping databases). /// @@ -48,6 +61,8 @@ public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(int major, int m public virtual NpgsqlDbContextOptionsBuilder UseRedshift(bool useRedshift = true) => WithOption(e => e.WithRedshift(useRedshift)); + #region MapRange + /// /// Maps a user-defined PostgreSQL range type for use. /// @@ -95,6 +110,39 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange( string? subtypeName = null) => WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName)); + #endregion MapRange + + #region MapEnum + + /// + /// 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 = null, + 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 = null, + string? schemaName = null, + INpgsqlNameTranslator? nameTranslator = null) + => WithOption(e => e.WithEnumMapping(clrType, enumName, schemaName, nameTranslator)); + + #endregion MapEnum + /// /// 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 @@ -104,12 +152,13 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange( internal virtual NpgsqlDbContextOptionsBuilder ReverseNullOrdering(bool reverseNullOrdering = true) => WithOption(e => e.WithReverseNullOrdering(reverseNullOrdering)); - #region Authentication + #region Authentication (obsolete) /// /// Configures the to use the specified . /// /// The callback to use. + [Obsolete("Call ConfigureDataSource() and configure the client certificates on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")] public virtual NpgsqlDbContextOptionsBuilder ProvideClientCertificatesCallback(ProvideClientCertificatesCallback? callback) => WithOption(e => e.WithProvideClientCertificatesCallback(callback)); @@ -117,6 +166,7 @@ public virtual NpgsqlDbContextOptionsBuilder ProvideClientCertificatesCallback(P /// Configures the to use the specified . /// /// The callback to use. + [Obsolete("Call ConfigureDataSource() and configure remote certificate validation on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")] public virtual NpgsqlDbContextOptionsBuilder RemoteCertificateValidationCallback(RemoteCertificateValidationCallback? callback) => WithOption(e => e.WithRemoteCertificateValidationCallback(callback)); @@ -124,12 +174,11 @@ public virtual NpgsqlDbContextOptionsBuilder RemoteCertificateValidationCallback /// Configures the to use the specified . /// /// The callback to use. -#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete + [Obsolete("Call ConfigureDataSource() and configure the password callback on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")] public virtual NpgsqlDbContextOptionsBuilder ProvidePasswordCallback(ProvidePasswordCallback? callback) => WithOption(e => e.WithProvidePasswordCallback(callback)); -#pragma warning restore CS0618 - #endregion Authentication + #endregion Authentication (obsolete) #region Retrying execution strategy diff --git a/src/EFCore.PG/Internal/EnumerableMethods.cs b/src/EFCore.PG/Internal/EnumerableMethods.cs index f840e16ff..3d8626534 100644 --- a/src/EFCore.PG/Internal/EnumerableMethods.cs +++ b/src/EFCore.PG/Internal/EnumerableMethods.cs @@ -257,14 +257,14 @@ static EnumerableMethods() All = GetMethod( nameof(Enumerable.All), 1, - types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool)) }); + types => [typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool))]); // AnyWithoutPredicate = GetMethod(nameof(Enumerable.Any), 1, // types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); AnyWithPredicate = GetMethod( nameof(Enumerable.Any), 1, - types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool)) }); + types => [typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool))]); // AsEnumerable = GetMethod(nameof(Enumerable.AsEnumerable), 1, // types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); @@ -280,7 +280,7 @@ static EnumerableMethods() Contains = GetMethod( nameof(Enumerable.Contains), 1, - types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]), types[0] }); + types => [typeof(IEnumerable<>).MakeGenericType(types[0]), types[0]]); // CountWithoutPredicate = GetMethod(nameof(Enumerable.Count), 1, // types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); @@ -325,7 +325,7 @@ static EnumerableMethods() // typeof(IEnumerable<>).MakeGenericType(types[0]) // }); - FirstWithoutPredicate = GetMethod(nameof(Enumerable.First), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); + FirstWithoutPredicate = GetMethod(nameof(Enumerable.First), 1, types => [typeof(IEnumerable<>).MakeGenericType(types[0])]); // FirstWithPredicate = GetMethod(nameof(Enumerable.First), 1, // types => new[] @@ -339,7 +339,7 @@ static EnumerableMethods() FirstOrDefaultWithPredicate = GetMethod( nameof(Enumerable.FirstOrDefault), 1, - types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool)) }); + types => [typeof(IEnumerable<>).MakeGenericType(types[0]), typeof(Func<,>).MakeGenericType(types[0], typeof(bool))]); // GroupByWithKeySelector = GetMethod(nameof(Enumerable.GroupBy), 2, // types => new[] @@ -645,6 +645,6 @@ MethodInfo GetMethod(string name, int genericParameterCount, Func ((genericParameterCount == 0 && !mi.IsGenericMethod) || (mi.IsGenericMethod && mi.GetGenericArguments().Length == genericParameterCount)) && mi.GetParameters().Select(e => e.ParameterType).SequenceEqual( - parameterGenerator(mi.IsGenericMethod ? mi.GetGenericArguments() : Array.Empty()))); + parameterGenerator(mi.IsGenericMethod ? mi.GetGenericArguments() : []))); } } diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs index 81785d0bf..5adba7a0c 100644 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs @@ -1,5 +1,4 @@ -using System.Data.Common; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; @@ -45,7 +44,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 +54,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,25 +62,21 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public NpgsqlSingletonOptions() { - UserRangeDefinitions = Array.Empty(); + EnumDefinitions = []; + UserRangeDefinitions = []; } /// 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 ?? coreOptions.ApplicationServiceProvider?.GetService(); } /// @@ -121,10 +108,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..1e1d22ae9 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs @@ -19,6 +19,7 @@ public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder { private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly Version _postgresVersion; + private readonly IReadOnlyList _enumDefinitions; /// /// Creates a new instance. @@ -36,6 +37,7 @@ public NpgsqlConventionSetBuilder( { _typeMappingSource = typeMappingSource; _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + _enumDefinitions = npgsqlSingletonOptions.EnumDefinitions; } /// @@ -70,7 +72,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/NpgsqlJsonElementHackConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlJsonElementHackConvention.cs index df2fb162a..dad88135f 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlJsonElementHackConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlJsonElementHackConvention.cs @@ -21,7 +21,7 @@ public void ProcessPropertyAdded(IConventionPropertyBuilder propertyBuilder, ICo { var property = propertyBuilder.Metadata; - if (property.ClrType == typeof(JsonElement) && property.GetColumnType() is null) + if (property.ClrType.UnwrapNullableType() == typeof(JsonElement) && property.GetColumnType() is null) { property.SetTypeMapping(_jsonTypeMapping ??= new NpgsqlJsonTypeMapping("jsonb", typeof(JsonElement))); } 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/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs index a03696320..a9493d6ae 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs @@ -97,20 +97,6 @@ public override IEnumerable For(IColumn column, bool designTime) } } - // If the property has a collation explicitly defined on it via the standard EF mechanism, it will get - // passed on the Collation property (we don't need to do anything). - // Otherwise, if a model-wide default column collation exists, pass that through our custom annotation. - // Note that this mechanism is obsolete, and EF Core's bulk model configuration can be used instead; but we continue to support - // it for backwards compat. -#pragma warning disable CS0618 - if (column.PropertyMappings.All(m => m.Property.GetCollation() is null) - && column.PropertyMappings.Select(m => m.Property.GetDefaultCollation()) - .FirstOrDefault(c => c is not null) is { } defaultColumnCollation) - { - yield return new Annotation(NpgsqlAnnotationNames.DefaultColumnCollation, defaultColumnCollation); - } -#pragma warning restore CS0618 - if (column.PropertyMappings.Select(m => m.Property.GetTsVectorConfig()) .FirstOrDefault(c => c is not null) is { } tsVectorConfig) { @@ -224,7 +210,7 @@ public override IEnumerable For(IRelationalModel model, bool design { if (!designTime) { - return Array.Empty(); + return []; } return model.Model.GetAnnotations().Where( 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/Internal/NpgsqlHistoryRepository.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs index 9cf6a2ed0..1366a1b6e 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs @@ -1,4 +1,6 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -6,8 +8,10 @@ /// 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 NpgsqlHistoryRepository : HistoryRepository +public class NpgsqlHistoryRepository : HistoryRepository, IHistoryRepository { + private IModel? _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 @@ -25,24 +29,59 @@ public NpgsqlHistoryRepository(HistoryRepositoryDependencies dependencies) /// 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 ExistsSql + public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Transaction; + + /// + /// 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 override IMigrationsDatabaseLock AcquireDatabaseLock() { - get - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - return - $""" -SELECT EXISTS ( - SELECT 1 FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace - WHERE n.nspname={stringTypeMapping.GenerateSqlLiteral(TableSchema ?? "public")} AND - c.relname={stringTypeMapping.GenerateSqlLiteral(TableName)} -) -"""; - } + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + + Dependencies.RawSqlCommandBuilder + .Build($"LOCK TABLE {Dependencies.SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} IN ACCESS EXCLUSIVE MODE") + .ExecuteNonQuery(CreateRelationalCommandParameters()); + + return new NpgsqlMigrationDatabaseLock(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 override async Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) + { + await Dependencies.RawSqlCommandBuilder + .Build($"LOCK TABLE {Dependencies.SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} IN ACCESS EXCLUSIVE MODE") + .ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken) + .ConfigureAwait(false); + + return new NpgsqlMigrationDatabaseLock(this); + } + + private RelationalCommandParameterObject CreateRelationalCommandParameters() + => new( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations); + + /// + /// 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 override string ExistsSql + => throw new UnreachableException( + "We should not be checking for the existence of the history table, but rather creating it and catching exceptions (see below)"); + /// /// 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 @@ -52,6 +91,96 @@ SELECT 1 FROM pg_catalog.pg_class c protected override bool InterpretExistsResult(object? value) => (bool?)value == true; + /// + /// 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 override IReadOnlyList GetCreateCommands() + { + // TODO: This is all a hack around https://github.com/dotnet/efcore/issues/34991: we have provider-specific conventions which add + // enums and extensions to the model, and the default EF logic causes them to be created at this point, when the history table is + // being created. + var model = EnsureModel(); + + var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel()); + var commandList = Dependencies.MigrationsSqlGenerator.Generate(operations, model); + return commandList; + } + + private IModel EnsureModel() + { + if (_model == null) + { + var conventionSet = Dependencies.ConventionSetBuilder.CreateConventionSet(); + + conventionSet.Remove(typeof(DbSetFindingConvention)); + conventionSet.Remove(typeof(RelationalDbFunctionAttributeConvention)); + // TODO: this whole method exists only so we can remove this convention (https://github.com/dotnet/efcore/issues/34991) + conventionSet.Remove(typeof(NpgsqlPostgresModelFinalizingConvention)); + + var modelBuilder = new ModelBuilder(conventionSet); + modelBuilder.Entity( + x => + { + ConfigureTable(x); + x.ToTable(TableName, TableSchema); + }); + + _model = Dependencies.ModelRuntimeInitializer.Initialize( + (IModel)modelBuilder.Model, designTime: true, validationLogger: null); + } + + return _model; + } + + bool IHistoryRepository.CreateIfNotExists() + { + // In PG, doing CREATE TABLE IF NOT EXISTS isn't concurrency-safe, and can result a "duplicate table" error or in a unique + // constraint violation (duplicate key value violates unique constraint "pg_type_typname_nsp_index"). + // We catch this and report that the table wasn't created. + try + { + return Dependencies.MigrationCommandExecutor.ExecuteNonQuery( + GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true) + != 0; + } + catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.UniqueViolation + or PostgresErrorCodes.DuplicateTable + or PostgresErrorCodes.DuplicateObject) + { + return false; + } + } + + async Task IHistoryRepository.CreateIfNotExistsAsync(CancellationToken cancellationToken) + { + // In PG, doing CREATE TABLE IF NOT EXISTS isn't concurrency-safe, and can result a "duplicate table" error or in a unique + // constraint violation (duplicate key value violates unique constraint "pg_type_typname_nsp_index"). + // We catch this and report that the table wasn't created. + try + { + return (await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( + GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, + cancellationToken: cancellationToken).ConfigureAwait(false)) + != 0; + } + catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.UniqueViolation + or PostgresErrorCodes.DuplicateTable + or PostgresErrorCodes.DuplicateObject) + { + return false; + } + } + + private IReadOnlyList GetCreateIfNotExistsCommands() + => Dependencies.MigrationsSqlGenerator.Generate([new SqlOperation + { + Sql = GetCreateIfNotExistsScript(), + SuppressTransaction = true + }]); + /// /// 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 @@ -61,7 +190,7 @@ protected override bool InterpretExistsResult(object? value) public override string GetCreateIfNotExistsScript() { var script = GetCreateScript(); - return script.Insert(script.IndexOf("CREATE TABLE", StringComparison.Ordinal) + 12, " IF NOT EXISTS"); + return script.Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); } /// @@ -71,10 +200,12 @@ public override string GetCreateIfNotExistsScript() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string GetBeginIfNotExistsScript(string migrationId) - => $@" + => $""" + DO $EF$ BEGIN - IF NOT EXISTS(SELECT 1 FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} WHERE ""{MigrationIdColumnName}"" = '{migrationId}') THEN"; + IF NOT EXISTS(SELECT 1 FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} WHERE "{MigrationIdColumnName}" = '{migrationId}') THEN +"""; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -83,10 +214,11 @@ public override string GetBeginIfNotExistsScript(string migrationId) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string GetBeginIfExistsScript(string migrationId) - => $@" + => $""" DO $EF$ BEGIN - IF EXISTS(SELECT 1 FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} WHERE ""{MigrationIdColumnName}"" = '{migrationId}') THEN"; + IF EXISTS(SELECT 1 FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} WHERE "{MigrationIdColumnName}" = '{migrationId}') THEN +"""; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -95,6 +227,78 @@ public override string GetBeginIfExistsScript(string migrationId) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string GetEndIfScript() - => @" END IF; -END $EF$;"; + => """ + END IF; +END $EF$; +"""; + + /// + /// Calls the base implementation, but catches "table not found" exceptions; we do this rather than try to detect whether the + /// migration table already exists (see override below), since it's difficult to reliably check if the + /// migration history table exists or not (because user may set PG search_path, which determines unqualified tables + /// references when creating, selecting). + /// + public override IReadOnlyList GetAppliedMigrations() + { + try + { + return base.GetAppliedMigrations(); + } + catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.InvalidCatalogName or PostgresErrorCodes.UndefinedTable) + { + return []; + } + } + + /// + /// Calls the base implementation, but catches "table not found" exceptions; we do this rather than try to detect whether the + /// migration table already exists (see override below), since it's difficult to reliably check if the + /// migration history table exists or not (because user may set PG search_path, which determines unqualified tables + /// references when creating, selecting). + /// + public override async Task> GetAppliedMigrationsAsync(CancellationToken cancellationToken = default) + { + try + { + return await base.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); + } + catch (PostgresException e) when (e.SqlState is PostgresErrorCodes.InvalidCatalogName or PostgresErrorCodes.UndefinedTable) + { + return []; + } + } + + /// + /// Always returns for PostgreSQL - it's difficult to reliably check if the migration history table + /// exists or not (because user may set PG search_path, which determines unqualified tables references when creating, + /// selecting). So we instead catch the "table doesn't exist" exceptions instead. + /// + public override bool Exists() + => true; + + /// + /// Always returns for PostgreSQL - it's difficult to reliably check if the migration history table + /// exists or not (because user may set PG search_path, which determines unqualified tables references when creating, + /// selecting). So we instead catch the "table doesn't exist" exceptions instead. + /// + public override Task ExistsAsync(CancellationToken cancellationToken = default) + => Task.FromResult(true); + + private sealed class NpgsqlMigrationDatabaseLock(IHistoryRepository historyRepository) : IMigrationsDatabaseLock + { + /// + /// 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 IHistoryRepository HistoryRepository => historyRepository; + + public void Dispose() + { + } + + public ValueTask DisposeAsync() + => default; + } } diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs index 6b7d1f16b..601af5604 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs @@ -36,10 +36,14 @@ public NpgsqlMigrator( IModelRuntimeInitializer modelRuntimeInitializer, IDiagnosticsLogger logger, IRelationalCommandDiagnosticsLogger commandLogger, - IDatabaseProvider databaseProvider) + IDatabaseProvider databaseProvider, + IMigrationsModelDiffer migrationsModelDiffer, + IDesignTimeModel designTimeModel, + IDbContextOptions contextOptions, + IExecutionStrategy executionStrategy) : base(migrationsAssembly, historyRepository, databaseCreator, migrationsSqlGenerator, rawSqlCommandBuilder, migrationCommandExecutor, connection, sqlGenerationHelper, currentContext, modelRuntimeInitializer, logger, - commandLogger, databaseProvider) + commandLogger, databaseProvider, migrationsModelDiffer, designTimeModel, contextOptions, executionStrategy) { _historyRepository = historyRepository; _connection = connection; @@ -51,7 +55,7 @@ public NpgsqlMigrator( /// 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 override void Migrate(string? targetMigration = null) + public override void Migrate(string? targetMigration) { var appliedMigrations = _historyRepository.GetAppliedMigrations(); @@ -60,17 +64,15 @@ public override void Migrate(string? targetMigration = null) PopulateMigrations( appliedMigrations.Select(t => t.MigrationId), targetMigration, - out var migrationsToApply, - out var migrationsToRevert, - out _); + out var migratorData); - if (migrationsToRevert.Count + migrationsToApply.Count == 0) + if (migratorData.RevertedMigrations.Count + migratorData.AppliedMigrations.Count == 0) { return; } // If a PostgreSQL extension, enum or range was added, we want Npgsql to reload all types at the ADO.NET level. - var migrations = migrationsToApply.Count > 0 ? migrationsToApply : migrationsToRevert; + var migrations = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; var reloadTypes = migrations .SelectMany(m => m.UpOperations) .OfType() @@ -78,14 +80,14 @@ public override void Migrate(string? targetMigration = null) if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) { - npgsqlConnection.Open(); + _connection.Open(); try { npgsqlConnection.ReloadTypes(); } - catch + finally { - npgsqlConnection.Close(); + _connection.Close(); } } } @@ -96,9 +98,7 @@ public override void Migrate(string? targetMigration = null) /// 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 override async Task MigrateAsync( - string? targetMigration = null, - CancellationToken cancellationToken = default) + public override async Task MigrateAsync(string? targetMigration, CancellationToken cancellationToken = default) { var appliedMigrations = await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); @@ -107,17 +107,15 @@ public override async Task MigrateAsync( PopulateMigrations( appliedMigrations.Select(t => t.MigrationId), targetMigration, - out var migrationsToApply, - out var migrationsToRevert, - out _); + out var migratorData); - if (migrationsToRevert.Count + migrationsToApply.Count == 0) + if (migratorData.RevertedMigrations.Count + migratorData.AppliedMigrations.Count == 0) { return; } // If a PostgreSQL extension, enum or range was added, we want Npgsql to reload all types at the ADO.NET level. - var migrations = migrationsToApply.Count > 0 ? migrationsToApply : migrationsToRevert; + var migrations = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; var reloadTypes = migrations .SelectMany(m => m.UpOperations) .OfType() @@ -125,14 +123,14 @@ public override async Task MigrateAsync( if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) { - await npgsqlConnection.OpenAsync(cancellationToken).ConfigureAwait(false); + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); try { - await npgsqlConnection.ReloadTypesAsync().ConfigureAwait(false); + await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); } - catch + finally { - await npgsqlConnection.CloseAsync().ConfigureAwait(false); + await _connection.CloseAsync().ConfigureAwait(false); } } } diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 212680376..fccab7d18 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -84,7 +84,7 @@ void AddSequenceBumpingForSeeding() NpgsqlValueGenerationStrategy.IdentityByDefaultColumn or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn or NpgsqlValueGenerationStrategy.SerialColumn)) - ?? Enumerable.Empty()) + ?? []) .Distinct() .ToArray(); @@ -116,12 +116,14 @@ or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn // e.g. negative values seeded) builder .AppendLine( - @$"{selectOrPerform} setval( + $""" +{selectOrPerform} setval( pg_get_serial_sequence('{table}', '{unquotedColumn}'), GREATEST( (SELECT MAX({column}) FROM {table}) + 1, nextval(pg_get_serial_sequence('{table}', '{unquotedColumn}'))), - false);"); + false); +"""); } builder.EndCommand(); @@ -182,8 +184,8 @@ protected override void Generate( using (builder.Indent()) { - base.CreateTableColumns(operation, model, builder); - base.CreateTableConstraints(operation, model, builder); + CreateTableColumns(operation, model, builder); + CreateTableConstraints(operation, model, builder); builder.AppendLine(); } @@ -454,9 +456,15 @@ protected override void Generate(AlterColumnOperation operation, IModel? model, .Append("TYPE ") .Append(type); - if (newCollation != oldCollation) + if (newCollation is not null) { - builder.Append(" COLLATE ").Append(DelimitIdentifier(newCollation ?? "default")); + builder.Append(" COLLATE ").Append(DelimitIdentifier(newCollation)); + } + else if (type == oldType) + { + // If the type is the same, make it more explicit that we're just resetting the collation to the default + // (this isn't really required) + builder.Append(" COLLATE ").Append(DelimitIdentifier("default")); } builder.AppendLine(";"); @@ -786,6 +794,49 @@ protected override void Generate(RenameSequenceOperation operation, IModel? mode EndStatement(builder); } + /// + protected override void SequenceOptions( + string? schema, + string name, + SequenceOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool forAlter) + { + var intTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(int)); + var longTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(long)); + + builder + .Append(" INCREMENT BY ") + .Append(intTypeMapping.GenerateSqlLiteral(operation.IncrementBy)); + + if (operation.MinValue != null) + { + builder + .Append(" MINVALUE ") + .Append(longTypeMapping.GenerateSqlLiteral(operation.MinValue)); + } + else if (forAlter) + { + builder + .Append(" NO MINVALUE"); + } + + if (operation.MaxValue != null) + { + builder + .Append(" MAXVALUE ") + .Append(longTypeMapping.GenerateSqlLiteral(operation.MaxValue)); + } + else if (forAlter) + { + builder + .Append(" NO MAXVALUE"); + } + + builder.Append(operation.IsCyclic ? " CYCLE" : " NO CYCLE"); + } + /// protected override void Generate(RestartSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) { @@ -902,7 +953,7 @@ protected override void Generate( } /// - protected override void IndexOptions(CreateIndexOperation operation, IModel? model, MigrationCommandListBuilder builder) + protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) { if (_postgresVersion.AtLeast(11) && operation[NpgsqlAnnotationNames.IndexInclude] is string[] { Length: > 0 } includeColumns) { @@ -1200,9 +1251,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))) @@ -1221,18 +1272,34 @@ protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, + "https://www.postgresql.org/docs/current/sql-altertype.html)"); } - for (var (newPos, oldPos) = (0, 0); newPos < newLabels.Count; newPos++) + for (var newPos = 0; newPos < newLabels.Count; newPos++) { var newLabel = newLabels[newPos]; - var oldLabel = oldPos < oldLabels.Count ? oldLabels[oldPos] : null; - - if (newLabel == oldLabel) + if (oldLabels.Contains(newLabel)) { - oldPos++; continue; } - GenerateAddEnumLabel(newEnum, newLabel, oldLabel, model, builder); + // We add the new label just after the last one we have in the new labels definition (when the last one is new, it will have + // just been added). + // If the new label happens to be the first one, add it before the first old label. Otherwise, if there are no old labels, + // just append the label (no before/after). + if (newPos == newLabels.Count - 1) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } + else if (newPos > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: newLabels[newPos - 1], model, builder); + } + else if (oldLabels.Count > 0) + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: oldLabels[0], afterLabel: null, model, builder); + } + else + { + GenerateAddEnumLabel(newEnum, newLabel, beforeLabel: null, afterLabel: null, model, builder); + } } } } @@ -1283,9 +1350,15 @@ protected virtual void GenerateAddEnumLabel( PostgresEnum enumType, string addedLabel, string? beforeLabel, + string? afterLabel, IModel? model, MigrationCommandListBuilder builder) { + if (beforeLabel is not null && afterLabel is not null) + { + throw new UnreachableException("Both beforeLabel and afterLabel can't be specified"); + } + var schema = enumType.Schema ?? model?.GetDefaultSchema(); builder @@ -1300,6 +1373,12 @@ protected virtual void GenerateAddEnumLabel( .Append(" BEFORE ") .Append(_stringTypeMapping.GenerateSqlLiteral(beforeLabel)); } + else if (afterLabel is not null) + { + builder + .Append(" AFTER ") + .Append(_stringTypeMapping.GenerateSqlLiteral(afterLabel)); + } builder.AppendLine(";"); @@ -1951,7 +2030,7 @@ private bool IsSystemColumn(string name) /// /// https://www.postgresql.org/docs/current/static/ddl-system-columns.html /// - private static readonly string[] SystemColumnNames = { "tableoid", "xmin", "cmin", "xmax", "cmax", "ctid" }; + private static readonly string[] SystemColumnNames = ["tableoid", "xmin", "cmin", "xmax", "cmax", "ctid"]; #endregion System column utilities @@ -2051,20 +2130,24 @@ private string DelimitIdentifier(string name, string? schema) private string IndexColumnList(IndexColumn[] columns, string? method) { - var isFirst = true; var builder = new StringBuilder(); for (var i = 0; i < columns.Length; i++) { - if (!isFirst) + var column = columns[i]; + + if (i > 0) { builder.Append(", "); } - var column = columns[i]; - builder.Append(DelimitIdentifier(column.Name)); + if (!string.IsNullOrEmpty(column.Collation)) + { + builder.Append(" COLLATE ").Append(DelimitIdentifier(column.Collation)); + } + if (!string.IsNullOrEmpty(column.Operator)) { var delimitedOperator = TryParseSchema(column.Operator, out var name, out var schema) @@ -2074,11 +2157,6 @@ private string IndexColumnList(IndexColumn[] columns, string? method) builder.Append(" ").Append(delimitedOperator); } - if (!string.IsNullOrEmpty(column.Collation)) - { - builder.Append(" COLLATE ").Append(DelimitIdentifier(column.Collation)); - } - // Of the built-in access methods, only btree (the default) supports // sorting, thus we only want to emit sort options for btree indexes. if (method is null or "btree") @@ -2105,8 +2183,6 @@ private string IndexColumnList(IndexColumn[] columns, string? method) } } } - - isFirst = false; } return builder.ToString(); @@ -2154,11 +2230,11 @@ private string ColumnsToTsVector( "json" => string.Join( " || ", columnGroup.Select( c => - $@"json_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '""all""')")), + $"""json_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '"all"')""")), "jsonb" => string.Join( " || ", columnGroup.Select( c => - $@"jsonb_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '""all""')")), + $"""jsonb_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '"all"')""")), "null" => throw new InvalidOperationException( $"Column or index {columnOrIndexName} refers to unknown column in tsvector definition"), _ => throw new ArgumentOutOfRangeException() @@ -2220,22 +2296,13 @@ private static IndexColumn[] GetIndexColumns(CreateIndexOperation operation) return columns; } - private readonly struct IndexColumn + private readonly struct IndexColumn(string name, string? @operator, string? collation, bool isDescending, NullSortOrder nullSortOrder) { - public IndexColumn(string name, string? @operator, string? collation, bool isDescending, NullSortOrder nullSortOrder) - { - Name = name; - Operator = @operator; - Collation = collation; - IsDescending = isDescending; - NullSortOrder = nullSortOrder; - } - - public string Name { get; } - public string? Operator { get; } - public string? Collation { get; } - public bool IsDescending { get; } - public NullSortOrder NullSortOrder { get; } + public string Name { get; } = name; + public string? Operator { get; } = @operator; + public string? Collation { get; } = collation; + public bool IsDescending { get; } = isDescending; + public NullSortOrder NullSortOrder { get; } = nullSortOrder; } #endregion diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs index 3bcd31e53..23e9455d7 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs +++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs @@ -1,11 +1,11 @@ // +#nullable enable + using System.Resources; using Microsoft.EntityFrameworkCore.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -#nullable enable - namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal { /// @@ -20,12 +20,10 @@ private static readonly ResourceManager _resourceManager = new ResourceManager("Npgsql.EntityFrameworkCore.PostgreSQL.Properties.NpgsqlStrings", typeof(NpgsqlStrings).Assembly); /// - /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + /// ConfigureDataSource() cannot be used when an externally-provided NpgsqlDataSource is passed to UseNpgsql(). Either perform all data source configuration on the external NpgsqlDataSource, or pass a connection string to UseNpgsql() and specify the data source configuration there. /// - public static string TwoDataSourcesInSameServiceProvider(object? useInternalServiceProvider) - => string.Format( - GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)), - useInternalServiceProvider); + public static string DataSourceAndConfigNotSupported + => GetString("DataSourceAndConfigNotSupported"); /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different compression methods. @@ -171,6 +169,14 @@ public static string StoredProcedureReturnValueNotSupported(object? entityType, GetString("StoredProcedureReturnValueNotSupported", nameof(entityType), nameof(sproc)), entityType, sproc); + /// + /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + /// + public static string TwoDataSourcesInSameServiceProvider(object? useInternalServiceProvider) + => string.Format( + GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)), + useInternalServiceProvider); + /// /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseSqlServer' call. /// diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx index 1759bc4a9..d40cedf4e 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.resx +++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx @@ -117,8 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + + ConfigureDataSource() cannot be used when an externally-provided NpgsqlDataSource is passed to UseNpgsql(). Either perform all data source configuration on the external NpgsqlDataSource, or pass a connection string to UseNpgsql() and specify the data source configuration there. '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different compression methods. @@ -247,4 +247,7 @@ The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support return values; use output parameters instead. + + Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + \ No newline at end of file diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlAggregateMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlAggregateMethodCallTranslatorProvider.cs index 27574ff2b..7f8daed18 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlAggregateMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlAggregateMethodCallTranslatorProvider.cs @@ -23,11 +23,10 @@ public NpgsqlAggregateMethodCallTranslatorProvider( var typeMappingSource = dependencies.RelationalTypeMappingSource; AddTranslators( - new IAggregateMethodCallTranslator[] - { - new NpgsqlQueryableAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), + [ + new NpgsqlQueryableAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), new NpgsqlStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), new NpgsqlMiscAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource, model) - }); + ]); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index 834dcd949..847da4c17 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -1,6 +1,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; @@ -33,6 +34,17 @@ public class NpgsqlArrayMethodTranslator : IMethodCallTranslator private static readonly MethodInfo Enumerable_SequenceEqual = typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .Single(m => m.Name == nameof(Enumerable.SequenceEqual) && m.GetParameters().Length == 2); + + // TODO: Enumerable Append and Concat are only here because primitive collections aren't handled in ExecuteUpdate, + // https://github.com/dotnet/efcore/issues/32494 + private static readonly MethodInfo Enumerable_Append = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Enumerable.Append) && m.GetParameters().Length == 2); + + private static readonly MethodInfo Enumerable_Concat = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Enumerable.Concat) && m.GetParameters().Length == 2); + // ReSharper restore InconsistentNaming #endregion Methods @@ -72,7 +84,7 @@ public NpgsqlArrayMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFacto { return _sqlExpressionFactory.Function( "get_byte", - new[] { arguments[0], arguments[1] }, + [arguments[0], arguments[1]], nullable: true, argumentsPropagateNullability: TrueArrays[2], typeof(byte)); @@ -127,9 +139,12 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) _sqlExpressionFactory.Subtract( _sqlExpressionFactory.Function( "array_position", - new[] { array, item }, + [array, item], nullable: true, - TrueArrays[2], + // array_position can return NULL even if both its arguments are non-nullable; + // this is currently the way to express that (see + // https://github.com/dotnet/efcore/pull/33814#issuecomment-2687857927). + FalseArrays[2], arrayOrList.Type), _sqlExpressionFactory.Constant(1)), _sqlExpressionFactory.Constant(-1)); @@ -147,14 +162,48 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) _sqlExpressionFactory.Subtract( _sqlExpressionFactory.Function( "array_position", - new[] { array, item, startIndex }, + [array, item, startIndex], nullable: true, - TrueArrays[3], + // array_position can return NULL even if both its arguments are non-nullable; + // this is currently the way to express that (see + // https://github.com/dotnet/efcore/pull/33814#issuecomment-2687857927). + FalseArrays[3], arrayOrList.Type), _sqlExpressionFactory.Constant(1)), _sqlExpressionFactory.Constant(-1)); } + // TODO: Enumerable Append and Concat are only here because primitive collections aren't handled in ExecuteUpdate, + // https://github.com/dotnet/efcore/issues/32494 + if (method.IsClosedFormOf(Enumerable_Append)) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + + return _sqlExpressionFactory.Function( + "array_append", + [array, item], + nullable: true, + TrueArrays[2], + arrayOrList.Type, + arrayOrList.TypeMapping); + } + + if (method.IsClosedFormOf(Enumerable_Concat)) + { + var inferredMapping = ExpressionExtensions.InferTypeMapping(arrayOrList, arguments[0]); + + return _sqlExpressionFactory.Function( + "array_cat", + [ + _sqlExpressionFactory.ApplyTypeMapping(arrayOrList, inferredMapping), + _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping) + ], + nullable: true, + TrueArrays[2], + arrayOrList.Type, + inferredMapping); + } + return null; } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs index 3a9dc4a24..4a6a22659 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs @@ -52,16 +52,15 @@ public NpgsqlByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactor // We have a byte value, but we need a bytea for PostgreSQL POSITION. var value = arguments[1] is SqlConstantExpression constantValue - ? (SqlExpression)_sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, typeMapping) + ? _sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, typeMapping) // Create bytea from non-constant byte: SELECT set_byte('\x00', 0, 8::smallint); : _sqlExpressionFactory.Function( "set_byte", - new[] - { + [ _sqlExpressionFactory.Constant(new[] { (byte)0 }, typeMapping), _sqlExpressionFactory.Constant(0), arguments[1] - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[3], typeof(byte[]), @@ -70,8 +69,8 @@ public NpgsqlByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactor return _sqlExpressionFactory.GreaterThan( PgFunctionExpression.CreateWithArgumentSeparators( "position", - new[] { value, source }, - new[] { "IN" }, // POSITION(x IN y) + [value, source], + ["IN"], // POSITION(x IN y) nullable: true, argumentsPropagateNullability: TrueArrays[2], builtIn: true, @@ -85,7 +84,7 @@ public NpgsqlByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactor return _sqlExpressionFactory.Convert( _sqlExpressionFactory.Function( "get_byte", - new[] { arguments[0], _sqlExpressionFactory.Constant(0) }, + [arguments[0], _sqlExpressionFactory.Constant(0)], nullable: true, argumentsPropagateNullability: TrueArrays[2], typeof(byte)), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs index 39ef22fdb..84c27eb54 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs @@ -17,8 +17,8 @@ public class NpgsqlConvertTranslator : IMethodCallTranslator [nameof(Convert.ToString)] = "text" }; - private static readonly List SupportedTypes = new() - { + private static readonly List SupportedTypes = + [ typeof(bool), typeof(byte), typeof(decimal), @@ -27,8 +27,9 @@ public class NpgsqlConvertTranslator : IMethodCallTranslator typeof(int), typeof(long), typeof(short), - typeof(string) - }; + typeof(string), + typeof(object) + ]; private static readonly List SupportedMethods = TypeMapping.Keys diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs index a709728ee..f3a44238f 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs @@ -27,8 +27,8 @@ public class NpgsqlDateTimeMemberTranslator : IMemberTranslator public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory) { _typeMappingSource = typeMappingSource; - _timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!; - _timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!; + _timestampMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp without time zone")!; + _timestampTzMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")!; _sqlExpressionFactory = sqlExpressionFactory; } @@ -56,6 +56,11 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo return translated; } + if (declaringType == typeof(DateOnly) && TranslateDateOnly(instance, member) is { } translated2) + { + return translated2; + } + if (member.Name == nameof(DateTime.Date)) { // Note that DateTime.Date returns a DateTime, not a DateOnly (introduced later); so we convert using date_trunc (which returns @@ -67,10 +72,10 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo switch (instance) { case { TypeMapping: NpgsqlTimestampTypeMapping }: - case { TypeMapping: NpgsqlTimestampTzTypeMapping } when NpgsqlTypeMappingSource.LegacyTimestampBehavior: + case { } when NpgsqlTypeMappingSource.LegacyTimestampBehavior: return _sqlExpressionFactory.Function( "date_trunc", - new[] { _sqlExpressionFactory.Constant("day"), instance }, + [_sqlExpressionFactory.Constant("day"), instance], nullable: true, argumentsPropagateNullability: TrueArrays[2], returnType, @@ -79,7 +84,7 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo case { TypeMapping: NpgsqlTimestampTzTypeMapping }: return _sqlExpressionFactory.Function( "date_trunc", - new[] { _sqlExpressionFactory.Constant("day"), instance, _sqlExpressionFactory.Constant("UTC") }, + [_sqlExpressionFactory.Constant("day"), instance, _sqlExpressionFactory.Constant("UTC")], nullable: true, argumentsPropagateNullability: TrueArrays[3], returnType, @@ -112,7 +117,7 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo nameof(DateTime.Today) => _sqlExpressionFactory.Function( "date_trunc", - new[] { _sqlExpressionFactory.Constant("day"), LocalNow() }, + [_sqlExpressionFactory.Constant("day"), LocalNow()], nullable: false, argumentsPropagateNullability: FalseArrays[2], typeof(DateTime), @@ -148,7 +153,7 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo SqlExpression UtcNow() => _sqlExpressionFactory.Function( "now", - Array.Empty(), + [], nullable: false, argumentsPropagateNullability: TrueArrays[0], returnType, @@ -172,7 +177,7 @@ SqlExpression LocalNow() var result = _sqlExpressionFactory.Function( "date_part", - new[] { _sqlExpressionFactory.Constant(partName), instance }, + [_sqlExpressionFactory.Constant(partName), instance], nullable: true, argumentsPropagateNullability: TrueArrays[2], typeof(double)); @@ -181,7 +186,7 @@ SqlExpression LocalNow() { result = _sqlExpressionFactory.Function( "floor", - new[] { result }, + [result], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(double)); @@ -190,13 +195,7 @@ SqlExpression LocalNow() return _sqlExpressionFactory.Convert(result, typeof(int)); } - /// - /// 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 SqlExpression? TranslateDateTimeOffset(SqlExpression instance, MemberInfo member) + private SqlExpression? TranslateDateTimeOffset(SqlExpression instance, MemberInfo member) => member.Name switch { // We only support UTC DateTimeOffset, so DateTimeOffset.DateTime is just a matter of converting to timestamp without time zone @@ -217,7 +216,7 @@ SqlExpression LocalNow() nameof(DateTimeOffset.Date) => _sqlExpressionFactory.Function( "date_trunc", - new SqlExpression[] { _sqlExpressionFactory.Constant("day"), _sqlExpressionFactory.AtUtc(instance) }, + [_sqlExpressionFactory.Constant("day"), _sqlExpressionFactory.AtUtc(instance)], nullable: true, argumentsPropagateNullability: TrueArrays[2], typeof(DateTime), @@ -226,6 +225,17 @@ SqlExpression LocalNow() _ => null }; + private SqlExpression? TranslateDateOnly(SqlExpression? instance, MemberInfo member) + => member.Name switch + { + // We use fragment rather than a DateOnly constant, since 0001-01-01 gets rendered as -infinity by default. + // TODO: Set the right type/type mapping after https://github.com/dotnet/efcore/pull/34995 is merged + nameof(DateOnly.DayNumber) when instance is not null + => _sqlExpressionFactory.Subtract(instance, _sqlExpressionFactory.Fragment("DATE '0001-01-01'")), + + _ => null + }; + // Various conversion functions translated here (date_part, ::time) exist only for timestamp without time zone, so if we pass in a // timestamptz it gets implicitly converted to a local timestamp based on TimeZone; that's the wrong behavior (these conversions are not // supposed to be sensitive to TimeZone). diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs index 960a7966e..2cb2a5d22 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs @@ -13,85 +13,89 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator { private static readonly Dictionary MethodInfoDatePartMapping = new() { - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) })!, "years" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), new[] { typeof(int) })!, "months" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddDays), new[] { typeof(double) })!, "days" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), new[] { typeof(double) })!, "hours" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) })!, "mins" }, - { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) })!, "secs" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), [typeof(int)])!, "years" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), [typeof(int)])!, "months" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddDays), [typeof(double)])!, "days" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), [typeof(double)])!, "hours" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), [typeof(double)])!, "mins" }, + { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), [typeof(double)])!, "secs" }, //{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), new[] { typeof(double) })!, "milliseconds" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), new[] { typeof(int) })!, "years" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), new[] { typeof(int) })!, "months" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), new[] { typeof(double) })!, "days" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddHours), new[] { typeof(double) })!, "hours" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), new[] { typeof(double) })!, "mins" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), new[] { typeof(double) })!, "secs" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), [typeof(int)])!, "years" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), [typeof(int)])!, "months" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), [typeof(double)])!, "days" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddHours), [typeof(double)])!, "hours" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), [typeof(double)])!, "mins" }, + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), [typeof(double)])!, "secs" }, //{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "milliseconds" } // DateOnly.AddDays, AddMonths and AddYears have a specialized translation, see below - { typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddHours), new[] { typeof(int) })!, "hours" }, - { typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddMinutes), new[] { typeof(int) })!, "mins" }, + { typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddHours), [typeof(int)])!, "hours" }, + { typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddMinutes), [typeof(int)])!, "mins" }, }; // ReSharper disable InconsistentNaming private static readonly MethodInfo DateTime_ToUniversalTime - = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToUniversalTime), Array.Empty())!; + = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToUniversalTime), [])!; private static readonly MethodInfo DateTime_ToLocalTime - = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToLocalTime), Array.Empty())!; + = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToLocalTime), [])!; private static readonly MethodInfo DateTime_SpecifyKind - = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.SpecifyKind), new[] { typeof(DateTime), typeof(DateTimeKind) })!; + = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.SpecifyKind), [typeof(DateTime), typeof(DateTimeKind)])!; private static readonly MethodInfo DateTime_Distance = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Distance), new[] { typeof(DbFunctions), typeof(DateTime), typeof(DateTime) })!; + nameof(NpgsqlDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateTime), typeof(DateTime)])!; private static readonly MethodInfo DateOnly_FromDateTime - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.FromDateTime), new[] { typeof(DateTime) })!; + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.FromDateTime), [typeof(DateTime)])!; private static readonly MethodInfo DateOnly_ToDateTime - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.ToDateTime), new[] { typeof(TimeOnly) })!; + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.ToDateTime), [typeof(TimeOnly)])!; private static readonly MethodInfo DateOnly_Distance = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Distance), new[] { typeof(DbFunctions), typeof(DateOnly), typeof(DateOnly) })!; + nameof(NpgsqlDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateOnly), typeof(DateOnly)])!; private static readonly MethodInfo DateOnly_AddDays - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) })!; + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), [typeof(int)])!; private static readonly MethodInfo DateOnly_AddMonths - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) })!; + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), [typeof(int)])!; private static readonly MethodInfo DateOnly_AddYears - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) })!; + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddYears), [typeof(int)])!; + + private static readonly MethodInfo DateOnly_FromDayNumber + = typeof(DateOnly).GetRuntimeMethod( + nameof(DateOnly.FromDayNumber), [typeof(int)])!; private static readonly MethodInfo TimeOnly_FromDateTime - = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.FromDateTime), new[] { typeof(DateTime) })!; + = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.FromDateTime), [typeof(DateTime)])!; private static readonly MethodInfo TimeOnly_FromTimeSpan - = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.FromTimeSpan), new[] { typeof(TimeSpan) })!; + = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.FromTimeSpan), [typeof(TimeSpan)])!; private static readonly MethodInfo TimeOnly_ToTimeSpan = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.ToTimeSpan), Type.EmptyTypes)!; private static readonly MethodInfo TimeOnly_IsBetween - = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.IsBetween), new[] { typeof(TimeOnly), typeof(TimeOnly) })!; + = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.IsBetween), [typeof(TimeOnly), typeof(TimeOnly)])!; private static readonly MethodInfo TimeOnly_Add_TimeSpan - = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), new[] { typeof(TimeSpan) })!; + = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), [typeof(TimeSpan)])!; private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTime = typeof(TimeZoneInfo).GetRuntimeMethod( - nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), new[] { typeof(DateTime), typeof(string) })!; + nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTime), typeof(string)])!; private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTimeOffset = typeof(TimeZoneInfo).GetRuntimeMethod( - nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), new[] { typeof(DateTimeOffset), typeof(string) })!; + nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTimeOffset), typeof(string)])!; private static readonly MethodInfo TimeZoneInfo_ConvertTimeToUtc - = typeof(TimeZoneInfo).GetRuntimeMethod(nameof(TimeZoneInfo.ConvertTimeToUtc), new[] { typeof(DateTime) })!; + = typeof(TimeZoneInfo).GetRuntimeMethod(nameof(TimeZoneInfo.ConvertTimeToUtc), [typeof(DateTime)])!; // ReSharper restore InconsistentNaming private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -113,9 +117,9 @@ public NpgsqlDateTimeMethodTranslator( { _typeMappingSource = typeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; - _timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!; - _timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!; - _intervalMapping = typeMappingSource.FindMapping("interval")!; + _timestampMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp without time zone")!; + _timestampTzMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")!; + _intervalMapping = typeMappingSource.FindMapping(typeof(TimeSpan), "interval")!; _textMapping = typeMappingSource.FindMapping("text")!; } @@ -226,6 +230,18 @@ public NpgsqlDateTimeMethodTranslator( { return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); } + + if (method == DateOnly_FromDayNumber) + { + // We use fragment rather than a DateOnly constant, since 0001-01-01 gets rendered as -infinity by default. + // TODO: Set the right type/type mapping after https://github.com/dotnet/efcore/pull/34995 is merged + return new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.Fragment("DATE '0001-01-01'"), + arguments[0], + typeof(DateOnly), + _typeMappingSource.FindMapping(typeof(DateOnly))); + } } else { diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs index d3e1cc758..e4db8fba8 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs @@ -111,12 +111,11 @@ public NpgsqlFullTextSearchMethodTranslator( 3 => _sqlExpressionFactory.Function( rankFunctionName, - new[] - { + [ arguments[1].Type == typeof(float[]) ? arguments[1] : arguments[0], arguments[1].Type == typeof(float[]) ? arguments[0] : arguments[1], arguments[2] - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[3], typeof(float), @@ -124,7 +123,7 @@ public NpgsqlFullTextSearchMethodTranslator( 4 => _sqlExpressionFactory.Function( rankFunctionName, - new[] { arguments[1], arguments[0], arguments[2], arguments[3] }, + [arguments[1], arguments[0], arguments[2], arguments[3]], nullable: true, argumentsPropagateNullability: TrueArrays[4], method.ReturnType, @@ -148,7 +147,7 @@ public NpgsqlFullTextSearchMethodTranslator( "setweight", newArgs, nullable: true, - argumentsPropagateNullability: TrueArrays[2], + argumentsPropagateNullability: TrueArrays[newArgs.Count], method.ReturnType); } @@ -181,7 +180,7 @@ public NpgsqlFullTextSearchMethodTranslator( arguments[1].Type == typeof(string) ? _sqlExpressionFactory.Function( "plainto_tsquery", - new[] { arguments[1] }, + [arguments[1]], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(NpgsqlTsQuery), @@ -211,7 +210,7 @@ public NpgsqlFullTextSearchMethodTranslator( nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 2 => _sqlExpressionFactory.Function( "ts_headline", - new[] { arguments[1], arguments[0] }, + [arguments[1], arguments[0]], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType), @@ -219,7 +218,7 @@ public NpgsqlFullTextSearchMethodTranslator( nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 3 => _sqlExpressionFactory.Function( "ts_headline", - new[] { arguments[1], arguments[0], arguments[2] }, + [arguments[1], arguments[0], arguments[2]], nullable: true, argumentsPropagateNullability: TrueArrays[3], method.ReturnType), @@ -227,8 +226,7 @@ public NpgsqlFullTextSearchMethodTranslator( nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 4 => _sqlExpressionFactory.Function( "ts_headline", - new[] - { + [ // For the regconfig parameter, if a constant string was provided, just pass it as a string - regconfig-accepting functions // will implicitly cast to regconfig. For (string!) parameters, we add an explicit cast, since regconfig actually is an OID // behind the scenes, and for parameter binary transfer no type coercion occurs. @@ -238,7 +236,7 @@ arguments[1] is SqlConstantExpression constant arguments[2], arguments[0], arguments[3] - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[4], method.ReturnType), @@ -310,8 +308,7 @@ arguments[1] is SqlConstantExpression constant SqlExpression ConfigAccepting(string functionName) => _sqlExpressionFactory.Function( - functionName, new[] - { + functionName, [ // For the regconfig parameter, if a constant string was provided, just pass it as a string - regconfig-accepting functions // will implicitly cast to regconfig. For (string!) parameters, we add an explicit cast, since regconfig actually is an OID // behind the scenes, and for parameter binary transfer no type coercion occurs. @@ -319,16 +316,15 @@ arguments[1] is SqlConstantExpression constant ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regconfigMapping), arguments[2] - }, + ], nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count], + argumentsPropagateNullability: TrueArrays[2], method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); SqlExpression DictionaryAccepting(string functionName) => _sqlExpressionFactory.Function( - functionName, new[] - { + functionName, [ // For the regdictionary parameter, if a constant string was provided, just pass it as a string - regdictionary-accepting functions // will implicitly cast to regdictionary. For (string!) parameters, we add an explicit cast, since regdictionary actually is an OID // behind the scenes, and for parameter binary transfer no type coercion occurs. @@ -336,18 +332,18 @@ arguments[1] is SqlConstantExpression constant ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regdictionaryMapping), arguments[2] - }, + ], nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count], + argumentsPropagateNullability: TrueArrays[2], method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); SqlExpression NonConfigAccepting(string functionName) => _sqlExpressionFactory.Function( functionName, - new[] { arguments[1] }, + [arguments[1]], nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count], + argumentsPropagateNullability: TrueArrays[1], method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs index 1e4b84a88..b0c6aa651 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs @@ -36,15 +36,15 @@ private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private static readonly bool[][] TrueArrays = - { - Array.Empty(), - new[] { true }, - new[] { true, true }, - new[] { true, true, true }, - new[] { true, true, true, true }, - new[] { true, true, true, true, true }, - new[] { true, true, true, true, true, true } - }; + [ + [], + [true], + [true, true], + [true, true, true], + [true, true, true, true], + [true, true, true, true, true], + [true, true, true, true, true, true] + ]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs index b1ee9e709..122b3e0c5 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs @@ -72,7 +72,7 @@ public NpgsqlJsonDbFunctionsTranslator( { return _sqlExpressionFactory.Function( ((NpgsqlJsonTypeMapping)args[0].TypeMapping!).IsJsonb ? "jsonb_typeof" : "json_typeof", - new[] { args[0] }, + [args[0]], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(string)); diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs index e57698740..ec838d19d 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs @@ -16,7 +16,7 @@ public class NpgsqlJsonDomTranslator : IMemberTranslator, IMethodCallTranslator private static readonly MemberInfo RootElement = typeof(JsonDocument).GetProperty(nameof(JsonDocument.RootElement))!; private static readonly MethodInfo GetProperty = typeof(JsonElement).GetRuntimeMethod( - nameof(JsonElement.GetProperty), new[] { typeof(string) })!; + nameof(JsonElement.GetProperty), [typeof(string)])!; private static readonly MethodInfo GetArrayLength = typeof(JsonElement).GetRuntimeMethod( nameof(JsonElement.GetArrayLength), Type.EmptyTypes)!; @@ -26,7 +26,7 @@ public class NpgsqlJsonDomTranslator : IMemberTranslator, IMethodCallTranslator .GetMethod!; private static readonly string[] GetMethods = - { + [ nameof(JsonElement.GetBoolean), nameof(JsonElement.GetDateTime), nameof(JsonElement.GetDateTimeOffset), @@ -38,7 +38,7 @@ public class NpgsqlJsonDomTranslator : IMemberTranslator, IMethodCallTranslator nameof(JsonElement.GetInt64), nameof(JsonElement.GetSingle), nameof(JsonElement.GetString) - }; + ]; private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; @@ -130,16 +130,18 @@ public NpgsqlJsonDomTranslator( typeof(string), _stringTypeMapping); + // The PostgreSQL traversal operator always returns text - for these scalar-returning methods, apply a conversion from string. return method.Name == nameof(JsonElement.GetString) ? traversalToText - : ConvertFromText(traversalToText, method.ReturnType); + : _sqlExpressionFactory.Convert( + traversalToText, method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); } if (method == GetArrayLength) { return _sqlExpressionFactory.Function( mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)); @@ -152,30 +154,4 @@ public NpgsqlJsonDomTranslator( return null; } - - // The PostgreSQL traversal operator always returns text, so we need to convert to int, bool, etc. - private SqlExpression ConvertFromText(SqlExpression expression, Type returnType) - { - switch (Type.GetTypeCode(returnType)) - { - case TypeCode.Boolean: - case TypeCode.Byte: - case TypeCode.DateTime: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return _sqlExpressionFactory.Convert(expression, returnType, _typeMappingSource.FindMapping(returnType, _model)); - default: - return returnType == typeof(Guid) - ? _sqlExpressionFactory.Convert(expression, returnType, _typeMappingSource.FindMapping(returnType, _model)) - : expression; - } - } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs index 0b4fe3015..fbaf19142 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs @@ -97,7 +97,8 @@ public NpgsqlJsonPocoTranslator( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlExpression? TranslateMemberAccess(SqlExpression instance, SqlExpression member, Type returnType) - => instance switch + { + return instance switch { // The first time we see a JSON traversal it's on a column - create a JsonTraversalExpression. // Traversals on top of that get appended into the same expression. @@ -105,7 +106,7 @@ public NpgsqlJsonPocoTranslator( => ConvertFromText( _sqlExpressionFactory.JsonTraversal( columnExpression, - new[] { member }, + [member], returnsText: true, typeof(string), _stringTypeMapping), @@ -119,6 +120,25 @@ PgJsonTraversalExpression prevPathTraversal _ => null }; + // The PostgreSQL traversal operator always returns text. + // If the type returned is a scalar (int, bool, etc.), we need to apply a conversion from string. + SqlExpression ConvertFromText(SqlExpression expression, Type returnType) + => _typeMappingSource.FindMapping(returnType.UnwrapNullableType(), _model) switch + { + // Type mapping not found - this isn't a scalar + null => expression, + + // Arrays are dealt with as JSON arrays, not array scalars + NpgsqlArrayTypeMapping => expression, + + // Text types don't a a conversion to string + { StoreTypeNameBase: "text" or "varchar" or "char" } => expression, + + // For any other type mapping, this is a scalar; apply a conversion to the type from string. + var mapping => _sqlExpressionFactory.Convert(expression, returnType, mapping) + }; + } + /// /// 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 @@ -132,9 +152,9 @@ PgJsonTraversalExpression prevPathTraversal case ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping mapping }: return _sqlExpressionFactory.Function( mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - new[] { expression }, + [expression], nullable: true, - argumentsPropagateNullability: TrueArrays[2], + argumentsPropagateNullability: TrueArrays[1], typeof(int)); case PgJsonTraversalExpression traversal: @@ -150,47 +170,13 @@ PgJsonTraversalExpression prevPathTraversal var jsonMapping = (NpgsqlJsonTypeMapping)traversal.Expression.TypeMapping!; return _sqlExpressionFactory.Function( jsonMapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - new[] { newTraversal }, + [newTraversal], nullable: true, - argumentsPropagateNullability: TrueArrays[2], + argumentsPropagateNullability: TrueArrays[1], typeof(int)); default: return null; } } - - // The PostgreSQL traversal operator always returns text, so we need to convert to int, bool, etc. - private SqlExpression ConvertFromText(SqlExpression expression, Type returnType) - { - var unwrappedReturnType = returnType.UnwrapNullableType(); - - switch (Type.GetTypeCode(unwrappedReturnType)) - { - case TypeCode.Boolean: - case TypeCode.Byte: - case TypeCode.DateTime: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return _sqlExpressionFactory.Convert(expression, returnType, _typeMappingSource.FindMapping(returnType, _model)); - } - - if (unwrappedReturnType == typeof(Guid) - || unwrappedReturnType == typeof(DateTimeOffset) - || unwrappedReturnType == typeof(DateOnly) - || unwrappedReturnType == typeof(TimeOnly)) - { - return _sqlExpressionFactory.Convert(expression, returnType, _typeMappingSource.FindMapping(returnType, _model)); - } - - return expression; - } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs index 9d9a55cb2..4875c2275 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs @@ -82,7 +82,7 @@ public NpgsqlLTreeTranslator( nameof(LTree.Subtree) => _sqlExpressionFactory.Function( "subltree", - new[] { instance!, arguments[0], arguments[1] }, + [instance!, arguments[0], arguments[1]], nullable: true, TrueArrays[3], typeof(LTree), @@ -92,7 +92,7 @@ public NpgsqlLTreeTranslator( => _sqlExpressionFactory.Function( "subpath", arguments.Count == 2 - ? new[] { instance!, arguments[0], arguments[1] } + ? [instance!, arguments[0], arguments[1]] : new[] { instance!, arguments[0] }, nullable: true, arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], @@ -103,7 +103,7 @@ public NpgsqlLTreeTranslator( => _sqlExpressionFactory.Function( "index", arguments.Count == 2 - ? new[] { instance!, arguments[0], arguments[1] } + ? [instance!, arguments[0], arguments[1]] : new[] { instance!, arguments[0] }, nullable: true, arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], @@ -112,7 +112,7 @@ public NpgsqlLTreeTranslator( nameof(LTree.LongestCommonAncestor) => _sqlExpressionFactory.Function( "lca", - new[] { arguments[0] }, + [arguments[0]], nullable: true, TrueArrays[1], typeof(LTree), @@ -139,7 +139,7 @@ public NpgsqlLTreeTranslator( => member.DeclaringType == typeof(LTree) && member.Name == nameof(LTree.NLevel) ? _sqlExpressionFactory.Function( "nlevel", - new[] { instance! }, + [instance!], nullable: true, TrueArrays[1], typeof(int)) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs index 470ce23ad..6aba0252d 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs @@ -8,24 +8,24 @@ public class NpgsqlLikeTranslator : IMethodCallTranslator private static readonly MethodInfo Like = typeof(DbFunctionsExtensions).GetRuntimeMethod( nameof(DbFunctionsExtensions.Like), - new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; + [typeof(DbFunctions), typeof(string), typeof(string)])!; private static readonly MethodInfo LikeWithEscape = typeof(DbFunctionsExtensions).GetRuntimeMethod( nameof(DbFunctionsExtensions.Like), - new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) })!; + [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; // ReSharper disable once InconsistentNaming private static readonly MethodInfo ILike = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlDbFunctionsExtensions.ILike), - new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; + [typeof(DbFunctions), typeof(string), typeof(string)])!; // ReSharper disable once InconsistentNaming private static readonly MethodInfo ILikeWithEscape = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlDbFunctionsExtensions.ILike), - new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) })!; + [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs index b7a14e134..07919734b 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs @@ -16,119 +16,119 @@ public class NpgsqlMathTranslator : IMethodCallTranslator { private static readonly Dictionary SupportedMethodTranslations = new() { - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(decimal) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(double) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(float) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(int) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(long) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), new[] { typeof(short) })!, "abs" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Abs), new[] { typeof(float) })!, "abs" }, - { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Abs), new[] { typeof(BigInteger) })!, "abs" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), new[] { typeof(decimal) })!, "ceiling" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), new[] { typeof(double) })!, "ceiling" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Ceiling), new[] { typeof(float) })!, "ceiling" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Floor), new[] { typeof(decimal) })!, "floor" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Floor), new[] { typeof(double) })!, "floor" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Floor), new[] { typeof(float) })!, "floor" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Pow), new[] { typeof(double), typeof(double) })!, "power" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Pow), new[] { typeof(float), typeof(float) })!, "power" }, - { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Pow), new[] { typeof(BigInteger), typeof(int) })!, "power" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Exp), new[] { typeof(double) })!, "exp" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Exp), new[] { typeof(float) })!, "exp" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Log10), new[] { typeof(double) })!, "log" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log10), new[] { typeof(float) })!, "log" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Log), new[] { typeof(double) })!, "ln" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log), new[] { typeof(float) })!, "ln" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(decimal)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(double)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(float)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(int)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(long)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(short)])!, "abs" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Abs), [typeof(float)])!, "abs" }, + { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Abs), [typeof(BigInteger)])!, "abs" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), [typeof(decimal)])!, "ceiling" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), [typeof(double)])!, "ceiling" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Ceiling), [typeof(float)])!, "ceiling" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Floor), [typeof(decimal)])!, "floor" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Floor), [typeof(double)])!, "floor" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Floor), [typeof(float)])!, "floor" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Pow), [typeof(double), typeof(double)])!, "power" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Pow), [typeof(float), typeof(float)])!, "power" }, + { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Pow), [typeof(BigInteger), typeof(int)])!, "power" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Exp), [typeof(double)])!, "exp" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Exp), [typeof(float)])!, "exp" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Log10), [typeof(double)])!, "log" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log10), [typeof(float)])!, "log" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Log), [typeof(double)])!, "ln" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log), [typeof(float)])!, "ln" }, // Note: PostgreSQL has log(x,y) but only for decimal, whereas .NET has it only for double/float - { typeof(Math).GetRuntimeMethod(nameof(Math.Sqrt), new[] { typeof(double) })!, "sqrt" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sqrt), new[] { typeof(float) })!, "sqrt" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Acos), new[] { typeof(double) })!, "acos" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Acos), new[] { typeof(float) })!, "acos" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Asin), new[] { typeof(double) })!, "asin" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Asin), new[] { typeof(float) })!, "asin" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Atan), new[] { typeof(double) })!, "atan" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan), new[] { typeof(float) })!, "atan" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Atan2), new[] { typeof(double), typeof(double) })!, "atan2" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan2), new[] { typeof(float), typeof(float) })!, "atan2" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Cos), new[] { typeof(double) })!, "cos" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Cos), new[] { typeof(float) })!, "cos" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Sin), new[] { typeof(double) })!, "sin" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sin), new[] { typeof(float) })!, "sin" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Tan), new[] { typeof(double) })!, "tan" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Tan), new[] { typeof(float) })!, "tan" }, - { typeof(double).GetRuntimeMethod(nameof(double.DegreesToRadians), new[] { typeof(double) })!, "radians" }, - { typeof(float).GetRuntimeMethod(nameof(float.DegreesToRadians), new[] { typeof(float) })!, "radians" }, - { typeof(double).GetRuntimeMethod(nameof(double.RadiansToDegrees), new[] { typeof(double) })!, "degrees" }, - { typeof(float).GetRuntimeMethod(nameof(float.RadiansToDegrees), new[] { typeof(float) })!, "degrees" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Sqrt), [typeof(double)])!, "sqrt" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sqrt), [typeof(float)])!, "sqrt" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Acos), [typeof(double)])!, "acos" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Acos), [typeof(float)])!, "acos" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Asin), [typeof(double)])!, "asin" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Asin), [typeof(float)])!, "asin" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Atan), [typeof(double)])!, "atan" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan), [typeof(float)])!, "atan" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Atan2), [typeof(double), typeof(double)])!, "atan2" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan2), [typeof(float), typeof(float)])!, "atan2" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Cos), [typeof(double)])!, "cos" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Cos), [typeof(float)])!, "cos" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Sin), [typeof(double)])!, "sin" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sin), [typeof(float)])!, "sin" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Tan), [typeof(double)])!, "tan" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Tan), [typeof(float)])!, "tan" }, + { typeof(double).GetRuntimeMethod(nameof(double.DegreesToRadians), [typeof(double)])!, "radians" }, + { typeof(float).GetRuntimeMethod(nameof(float.DegreesToRadians), [typeof(float)])!, "radians" }, + { typeof(double).GetRuntimeMethod(nameof(double.RadiansToDegrees), [typeof(double)])!, "degrees" }, + { typeof(float).GetRuntimeMethod(nameof(float.RadiansToDegrees), [typeof(float)])!, "degrees" }, // https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(decimal), typeof(decimal) })!, "GREATEST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(double), typeof(double) })!, "GREATEST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(float), typeof(float) })!, "GREATEST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(int), typeof(int) })!, "GREATEST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(long), typeof(long) })!, "GREATEST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Max), new[] { typeof(short), typeof(short) })!, "GREATEST" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Max), new[] { typeof(float), typeof(float) })!, "GREATEST" }, - { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Max), new[] { typeof(BigInteger), typeof(BigInteger) })!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(decimal), typeof(decimal)])!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(double), typeof(double)])!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(float), typeof(float)])!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(int), typeof(int)])!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(long), typeof(long)])!, "GREATEST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Max), [typeof(short), typeof(short)])!, "GREATEST" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Max), [typeof(float), typeof(float)])!, "GREATEST" }, + { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Max), [typeof(BigInteger), typeof(BigInteger)])!, "GREATEST" }, // https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(decimal), typeof(decimal) })!, "LEAST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(double), typeof(double) })!, "LEAST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(float), typeof(float) })!, "LEAST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(int), typeof(int) })!, "LEAST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(long), typeof(long) })!, "LEAST" }, - { typeof(Math).GetRuntimeMethod(nameof(Math.Min), new[] { typeof(short), typeof(short) })!, "LEAST" }, - { typeof(MathF).GetRuntimeMethod(nameof(MathF.Min), new[] { typeof(float), typeof(float) })!, "LEAST" }, - { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Min), new[] { typeof(BigInteger), typeof(BigInteger) })!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(decimal), typeof(decimal)])!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(double), typeof(double)])!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(float), typeof(float)])!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(int), typeof(int)])!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(long), typeof(long)])!, "LEAST" }, + { typeof(Math).GetRuntimeMethod(nameof(Math.Min), [typeof(short), typeof(short)])!, "LEAST" }, + { typeof(MathF).GetRuntimeMethod(nameof(MathF.Min), [typeof(float), typeof(float)])!, "LEAST" }, + { typeof(BigInteger).GetRuntimeMethod(nameof(BigInteger.Min), [typeof(BigInteger), typeof(BigInteger)])!, "LEAST" }, }; - private static readonly IEnumerable TruncateMethodInfos = new[] - { + private static readonly IEnumerable TruncateMethodInfos = + [ typeof(Math).GetRequiredRuntimeMethod(nameof(Math.Truncate), typeof(decimal)), typeof(Math).GetRequiredRuntimeMethod(nameof(Math.Truncate), typeof(double)), typeof(MathF).GetRequiredRuntimeMethod(nameof(MathF.Truncate), typeof(float)) - }; + ]; - private static readonly IEnumerable RoundMethodInfos = new[] - { + private static readonly IEnumerable RoundMethodInfos = + [ typeof(Math).GetRequiredRuntimeMethod(nameof(Math.Round), typeof(decimal)), typeof(Math).GetRequiredRuntimeMethod(nameof(Math.Round), typeof(double)), typeof(MathF).GetRequiredRuntimeMethod(nameof(MathF.Round), typeof(float)) - }; - - private static readonly IEnumerable SignMethodInfos = new[] - { - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(decimal) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(double) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(float) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(int) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(long) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(sbyte) })!, - typeof(Math).GetRuntimeMethod(nameof(Math.Sign), new[] { typeof(short) })!, - typeof(MathF).GetRuntimeMethod(nameof(MathF.Sign), new[] { typeof(float) })!, - }; + ]; + + private static readonly IEnumerable SignMethodInfos = + [ + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(decimal)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(double)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(float)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(int)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(long)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(sbyte)])!, + typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(short)])!, + typeof(MathF).GetRuntimeMethod(nameof(MathF.Sign), [typeof(float)])! + ]; private static readonly MethodInfo RoundDecimalTwoParams - = typeof(Math).GetRuntimeMethod(nameof(Math.Round), new[] { typeof(decimal), typeof(int) })!; + = typeof(Math).GetRuntimeMethod(nameof(Math.Round), [typeof(decimal), typeof(int)])!; private static readonly MethodInfo DoubleIsNanMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsNaN), new[] { typeof(double) })!; + = typeof(double).GetRuntimeMethod(nameof(double.IsNaN), [typeof(double)])!; private static readonly MethodInfo DoubleIsPositiveInfinityMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsPositiveInfinity), new[] { typeof(double) })!; + = typeof(double).GetRuntimeMethod(nameof(double.IsPositiveInfinity), [typeof(double)])!; private static readonly MethodInfo DoubleIsNegativeInfinityMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsNegativeInfinity), new[] { typeof(double) })!; + = typeof(double).GetRuntimeMethod(nameof(double.IsNegativeInfinity), [typeof(double)])!; private static readonly MethodInfo FloatIsNanMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsNaN), new[] { typeof(float) })!; + = typeof(float).GetRuntimeMethod(nameof(float.IsNaN), [typeof(float)])!; private static readonly MethodInfo FloatIsPositiveInfinityMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsPositiveInfinity), new[] { typeof(float) })!; + = typeof(float).GetRuntimeMethod(nameof(float.IsPositiveInfinity), [typeof(float)])!; private static readonly MethodInfo FloatIsNegativeInfinityMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsNegativeInfinity), new[] { typeof(float) })!; + = typeof(float).GetRuntimeMethod(nameof(float.IsNegativeInfinity), [typeof(float)])!; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly RelationalTypeMapping _intTypeMapping; @@ -188,11 +188,11 @@ public NpgsqlMathTranslator( // C# has Round over decimal/double/float only so our argument will be one of those types (compiler puts convert node) // In database result will be same type except for float which returns double which we need to cast back to float. - var result = (SqlExpression)_sqlExpressionFactory.Function( + var result = _sqlExpressionFactory.Function( "trunc", - new[] { argument }, + [argument], nullable: true, - argumentsPropagateNullability: new[] { true, false, false }, + argumentsPropagateNullability: TrueArrays[1], argument.Type == typeof(float) ? typeof(double) : argument.Type); if (argument.Type == typeof(float)) @@ -209,11 +209,11 @@ public NpgsqlMathTranslator( // C# has Round over decimal/double/float only so our argument will be one of those types (compiler puts convert node) // In database result will be same type except for float which returns double which we need to cast back to float. - var result = (SqlExpression)_sqlExpressionFactory.Function( + var result = _sqlExpressionFactory.Function( "round", - new[] { argument }, + [argument], nullable: true, - argumentsPropagateNullability: new[] { true, true }, + argumentsPropagateNullability: TrueArrays[1], argument.Type == typeof(float) ? typeof(double) : argument.Type); if (argument.Type == typeof(float)) @@ -244,11 +244,10 @@ public NpgsqlMathTranslator( { return _sqlExpressionFactory.Function( "round", - new[] - { + [ _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType, diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs index 18db4afb2..28ab9785a 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs @@ -35,16 +35,15 @@ public NpgsqlMemberTranslatorProvider( JsonPocoTranslator = new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model); AddTranslators( - new IMemberTranslator[] - { - new NpgsqlBigIntegerMemberTranslator(sqlExpressionFactory), + [ + new NpgsqlBigIntegerMemberTranslator(sqlExpressionFactory), new NpgsqlDateTimeMemberTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory, model), JsonPocoTranslator, new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), new NpgsqlStringMemberTranslator(sqlExpressionFactory), - new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory), - }); + new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory) + ]); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 46a19b4b5..63843eab3 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -40,9 +40,8 @@ public NpgsqlMethodCallTranslatorProvider( LTreeTranslator = new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory, model); AddTranslators( - new IMethodCallTranslator[] - { - new NpgsqlArrayMethodTranslator(sqlExpressionFactory, jsonTranslator), + [ + new NpgsqlArrayMethodTranslator(sqlExpressionFactory, jsonTranslator), new NpgsqlByteArrayMethodTranslator(sqlExpressionFactory), new NpgsqlConvertTranslator(sqlExpressionFactory), new NpgsqlDateTimeMethodTranslator(typeMappingSource, sqlExpressionFactory), @@ -62,7 +61,7 @@ public NpgsqlMethodCallTranslatorProvider( new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory), new NpgsqlRowValueTranslator(sqlExpressionFactory), new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory), - new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), - }); + new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model) + ]); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs index 2e3077f7c..b938ecb96 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs @@ -12,10 +12,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte public class NpgsqlMiscAggregateMethodTranslator : IAggregateMethodCallTranslator { private static readonly MethodInfo StringJoin - = typeof(string).GetRuntimeMethod(nameof(string.Join), new[] { typeof(string), typeof(IEnumerable) })!; + = typeof(string).GetRuntimeMethod(nameof(string.Join), [typeof(string), typeof(IEnumerable)])!; private static readonly MethodInfo StringConcat - = typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(IEnumerable) })!; + = typeof(string).GetRuntimeMethod(nameof(string.Concat), [typeof(IEnumerable)])!; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -71,14 +71,15 @@ public NpgsqlMiscAggregateMethodTranslator( return _sqlExpressionFactory.Coalesce( _sqlExpressionFactory.AggregateFunction( "string_agg", - new[] - { + [ sqlExpression, method == StringJoin ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string)) - }, + ], source, nullable: true, - argumentsPropagateNullability: new[] { false, true }, + // string_agg can return nulls regardless of the nullability of its arguments, since if there's an aggregate predicate + // (string_agg(...) WHERE ...), it could cause there to be no elements, in which case string_agg returns null. + argumentsPropagateNullability: FalseArrays[2], typeof(string), _typeMappingSource.FindMapping("text")), // Note that string_agg returns text even if its inputs are varchar(x) _sqlExpressionFactory.Constant(string.Empty, typeof(string))); @@ -91,7 +92,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlAggregateDbFunctionsExtensions.ArrayAgg): return _sqlExpressionFactory.AggregateFunction( "array_agg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -103,7 +104,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonAgg): return _sqlExpressionFactory.AggregateFunction( "json_agg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -113,7 +114,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbAgg): return _sqlExpressionFactory.AggregateFunction( "jsonb_agg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -123,7 +124,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlAggregateDbFunctionsExtensions.Sum): return _sqlExpressionFactory.AggregateFunction( "sum", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -133,7 +134,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlAggregateDbFunctionsExtensions.Average): return _sqlExpressionFactory.AggregateFunction( "avg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -156,7 +157,7 @@ public NpgsqlMiscAggregateMethodTranslator( return _sqlExpressionFactory.AggregateFunction( isJsonb ? "jsonb_object_agg" : "json_object_agg", - new[] { keys, values }, + [keys, values], source, nullable: true, argumentsPropagateNullability: FalseArrays[2], @@ -174,7 +175,7 @@ public NpgsqlMiscAggregateMethodTranslator( return _sqlExpressionFactory.AggregateFunction( "range_agg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -184,7 +185,7 @@ public NpgsqlMiscAggregateMethodTranslator( case nameof(NpgsqlRangeDbFunctionsExtensions.RangeIntersectAgg): return _sqlExpressionFactory.AggregateFunction( "range_intersect_agg", - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs index 81ffb280f..3f607894f 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs @@ -14,10 +14,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte public class NpgsqlNetworkTranslator : IMethodCallTranslator { private static readonly MethodInfo IPAddressParse = - typeof(IPAddress).GetRuntimeMethod(nameof(IPAddress.Parse), new[] { typeof(string) })!; + typeof(IPAddress).GetRuntimeMethod(nameof(IPAddress.Parse), [typeof(string)])!; private static readonly MethodInfo PhysicalAddressParse = - typeof(PhysicalAddress).GetRuntimeMethod(nameof(PhysicalAddress.Parse), new[] { typeof(string) })!; + typeof(PhysicalAddress).GetRuntimeMethod(nameof(PhysicalAddress.Parse), [typeof(string)])!; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; @@ -180,31 +180,31 @@ public NpgsqlNetworkTranslator( _longAddressMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.Abbreviate) - => NullPropagatingFunction("abbrev", new[] { arguments[1] }, typeof(string)), + => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), nameof(NpgsqlNetworkDbFunctionsExtensions.Broadcast) - => NullPropagatingFunction("broadcast", new[] { arguments[1] }, typeof(IPAddress), _inetMapping), + => NullPropagatingFunction("broadcast", [arguments[1]], typeof(IPAddress), _inetMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.Family) - => NullPropagatingFunction("family", new[] { arguments[1] }, typeof(int)), + => NullPropagatingFunction("family", [arguments[1]], typeof(int)), nameof(NpgsqlNetworkDbFunctionsExtensions.Host) - => NullPropagatingFunction("host", new[] { arguments[1] }, typeof(string)), + => NullPropagatingFunction("host", [arguments[1]], typeof(string)), nameof(NpgsqlNetworkDbFunctionsExtensions.HostMask) - => NullPropagatingFunction("hostmask", new[] { arguments[1] }, typeof(IPAddress), _inetMapping), + => NullPropagatingFunction("hostmask", [arguments[1]], typeof(IPAddress), _inetMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.MaskLength) - => NullPropagatingFunction("masklen", new[] { arguments[1] }, typeof(int)), + => NullPropagatingFunction("masklen", [arguments[1]], typeof(int)), nameof(NpgsqlNetworkDbFunctionsExtensions.Netmask) - => NullPropagatingFunction("netmask", new[] { arguments[1] }, typeof(IPAddress), _inetMapping), + => NullPropagatingFunction("netmask", [arguments[1]], typeof(IPAddress), _inetMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.Network) - => NullPropagatingFunction("network", new[] { arguments[1] }, typeof((IPAddress Address, int Subnet)), _cidrMapping), + => NullPropagatingFunction("network", [arguments[1]], typeof((IPAddress Address, int Subnet)), _cidrMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.SetMaskLength) => NullPropagatingFunction( - "set_masklen", new[] { arguments[1], arguments[2] }, arguments[1].Type, arguments[1].TypeMapping), + "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.Text) - => NullPropagatingFunction("text", new[] { arguments[1] }, typeof(string)), + => NullPropagatingFunction("text", [arguments[1]], typeof(string)), nameof(NpgsqlNetworkDbFunctionsExtensions.SameFamily) - => NullPropagatingFunction("inet_same_family", new[] { arguments[1], arguments[2] }, typeof(bool)), + => NullPropagatingFunction("inet_same_family", [arguments[1], arguments[2]], typeof(bool)), nameof(NpgsqlNetworkDbFunctionsExtensions.Merge) => NullPropagatingFunction( - "inet_merge", new[] { arguments[1], arguments[2] }, typeof((IPAddress Address, int Subnet)), _cidrMapping), + "inet_merge", [arguments[1], arguments[2]], typeof((IPAddress Address, int Subnet)), _cidrMapping), _ => null }; @@ -213,10 +213,10 @@ public NpgsqlNetworkTranslator( => method.Name switch { nameof(NpgsqlNetworkDbFunctionsExtensions.Abbreviate) - => NullPropagatingFunction("abbrev", new[] { arguments[1] }, typeof(string)), + => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), nameof(NpgsqlNetworkDbFunctionsExtensions.SetMaskLength) => NullPropagatingFunction( - "set_masklen", new[] { arguments[1], arguments[2] }, arguments[1].Type, arguments[1].TypeMapping), + "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), _ => null }; @@ -241,14 +241,14 @@ public NpgsqlNetworkTranslator( => _sqlExpressionFactory.Or(arguments[1], arguments[2]), nameof(NpgsqlNetworkDbFunctionsExtensions.Truncate) => NullPropagatingFunction( - "trunc", new[] { arguments[1] }, typeof(PhysicalAddress), arguments[1].TypeMapping), + "trunc", [arguments[1]], typeof(PhysicalAddress), arguments[1].TypeMapping), nameof(NpgsqlNetworkDbFunctionsExtensions.Set7BitMac8) => NullPropagatingFunction( - "macaddr8_set7bit", new[] { arguments[1] }, typeof(PhysicalAddress), _macaddr8Mapping), + "macaddr8_set7bit", [arguments[1]], typeof(PhysicalAddress), _macaddr8Mapping), _ => null }; - private SqlFunctionExpression NullPropagatingFunction( + private SqlExpression NullPropagatingFunction( string name, SqlExpression[] arguments, Type returnType, diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs index 6ea22c4d8..8762343ff 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNewGuidTranslator.cs @@ -10,7 +10,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// public class NpgsqlNewGuidTranslator : IMethodCallTranslator { - private static readonly MethodInfo MethodInfo = typeof(Guid).GetRuntimeMethod(nameof(Guid.NewGuid), Array.Empty())!; + private static readonly MethodInfo MethodInfo = typeof(Guid).GetRuntimeMethod(nameof(Guid.NewGuid), [])!; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly string _uuidGenerationFunction; @@ -41,7 +41,7 @@ public NpgsqlNewGuidTranslator(ISqlExpressionFactory sqlExpressionFactory, Versi => MethodInfo.Equals(method) ? _sqlExpressionFactory.Function( _uuidGenerationFunction, - Array.Empty(), + [], nullable: false, argumentsPropagateNullability: FalseArrays[0], method.ReturnType) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs index 94df91dee..2a98b58ae 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs @@ -10,8 +10,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// public class NpgsqlObjectToStringTranslator : IMethodCallTranslator { - private static readonly HashSet _typeMapping = new() - { + private static readonly HashSet _typeMapping = + [ typeof(int), typeof(long), typeof(DateTime), @@ -29,8 +29,8 @@ public class NpgsqlObjectToStringTranslator : IMethodCallTranslator typeof(uint), typeof(ushort), typeof(ulong), - typeof(sbyte), - }; + typeof(sbyte) + ]; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly RelationalTypeMapping _textTypeMapping; @@ -67,31 +67,38 @@ public NpgsqlObjectToStringTranslator(IRelationalTypeMappingSource typeMappingSo if (instance.Type == typeof(bool)) { - return instance is ColumnExpression { IsNullable: true } - ? _sqlExpressionFactory.Case( - new[] - { - new CaseWhenClause( - _sqlExpressionFactory.Equal(instance, _sqlExpressionFactory.Constant(false)), - _sqlExpressionFactory.Constant(false.ToString())), + if (instance.Type == typeof(bool)) + { + if (instance is not ColumnExpression { IsNullable: false }) + { + return _sqlExpressionFactory.Case( + instance, + [ + new CaseWhenClause( + _sqlExpressionFactory.Constant(false), + _sqlExpressionFactory.Constant(false.ToString())), + new CaseWhenClause( + _sqlExpressionFactory.Constant(true), + _sqlExpressionFactory.Constant(true.ToString())) + ], + _sqlExpressionFactory.Constant(string.Empty)); + } + + return _sqlExpressionFactory.Case( + [ new CaseWhenClause( - _sqlExpressionFactory.Equal(instance, _sqlExpressionFactory.Constant(true)), + instance, _sqlExpressionFactory.Constant(true.ToString())) - }, - _sqlExpressionFactory.Constant(null)) - : _sqlExpressionFactory.Case( - new[] - { - new CaseWhenClause( - _sqlExpressionFactory.Equal(instance, _sqlExpressionFactory.Constant(false)), - _sqlExpressionFactory.Constant(false.ToString())) - }, - _sqlExpressionFactory.Constant(true.ToString())); + ], + _sqlExpressionFactory.Constant(false.ToString())); + } } return _typeMapping.Contains(instance.Type) || instance.Type.UnwrapNullableType().IsEnum && instance.TypeMapping is NpgsqlEnumTypeMapping - ? _sqlExpressionFactory.Convert(instance, typeof(string), _textTypeMapping) + ? _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Convert(instance, typeof(string), _textTypeMapping), + _sqlExpressionFactory.Constant(string.Empty)) : null; } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs index b1d8fae15..1cffc45fe 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs @@ -62,7 +62,7 @@ public NpgsqlQueryableAggregateMethodTranslator( ? _sqlExpressionFactory.Convert( _sqlExpressionFactory.AggregateFunction( "avg", - new[] { averageSqlExpression }, + [averageSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -71,7 +71,7 @@ public NpgsqlQueryableAggregateMethodTranslator( averageSqlExpression.TypeMapping) : _sqlExpressionFactory.AggregateFunction( "avg", - new[] { averageSqlExpression }, + [averageSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -86,7 +86,7 @@ public NpgsqlQueryableAggregateMethodTranslator( return _sqlExpressionFactory.Convert( _sqlExpressionFactory.AggregateFunction( "count", - new[] { countSqlExpression }, + [countSqlExpression], source, nullable: false, argumentsPropagateNullability: FalseArrays[1], @@ -100,7 +100,7 @@ public NpgsqlQueryableAggregateMethodTranslator( var longCountSqlExpression = (source.Selector as SqlExpression) ?? _sqlExpressionFactory.Fragment("*"); return _sqlExpressionFactory.AggregateFunction( "count", - new[] { longCountSqlExpression }, + [longCountSqlExpression], source, nullable: false, argumentsPropagateNullability: FalseArrays[1], @@ -112,7 +112,7 @@ public NpgsqlQueryableAggregateMethodTranslator( && source.Selector is SqlExpression maxSqlExpression: return _sqlExpressionFactory.AggregateFunction( "max", - new[] { maxSqlExpression }, + [maxSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -125,7 +125,7 @@ public NpgsqlQueryableAggregateMethodTranslator( && source.Selector is SqlExpression minSqlExpression: return _sqlExpressionFactory.AggregateFunction( "min", - new[] { minSqlExpression }, + [minSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -147,7 +147,7 @@ public NpgsqlQueryableAggregateMethodTranslator( return _sqlExpressionFactory.Convert( _sqlExpressionFactory.AggregateFunction( "sum", - new[] { sumSqlExpression }, + [sumSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -161,7 +161,7 @@ public NpgsqlQueryableAggregateMethodTranslator( return _sqlExpressionFactory.Convert( _sqlExpressionFactory.AggregateFunction( "sum", - new[] { sumSqlExpression }, + [sumSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -172,7 +172,7 @@ public NpgsqlQueryableAggregateMethodTranslator( return _sqlExpressionFactory.AggregateFunction( "sum", - new[] { sumSqlExpression }, + [sumSqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs index 2457ff1fa..3368c9a65 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs @@ -9,7 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte public class NpgsqlRandomTranslator : IMethodCallTranslator { private static readonly MethodInfo _methodInfo - = typeof(DbFunctionsExtensions).GetRuntimeMethod(nameof(DbFunctionsExtensions.Random), new[] { typeof(DbFunctions) })!; + = typeof(DbFunctionsExtensions).GetRuntimeMethod(nameof(DbFunctionsExtensions.Random), [typeof(DbFunctions)])!; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -43,9 +43,9 @@ public NpgsqlRandomTranslator(ISqlExpressionFactory sqlExpressionFactory) return _methodInfo.Equals(method) ? _sqlExpressionFactory.Function( "random", - Array.Empty(), + [], nullable: false, - argumentsPropagateNullability: Array.Empty(), + argumentsPropagateNullability: [], method.ReturnType) : null; } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs index 4a8cae301..2b6747fdf 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs @@ -56,7 +56,7 @@ public NpgsqlRangeTranslator( return _sqlExpressionFactory.Not( _sqlExpressionFactory.Function( "isempty", - new[] { arguments[0] }, + [arguments[0]], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(bool))); @@ -76,11 +76,10 @@ public NpgsqlRangeTranslator( return _sqlExpressionFactory.Function( "range_merge", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping), _sqlExpressionFactory.ApplyTypeMapping(arguments[1], inferredMapping) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType, @@ -95,7 +94,7 @@ public NpgsqlRangeTranslator( return _sqlExpressionFactory.Function( "range_merge", - new[] { arguments[0] }, + [arguments[0]], nullable: true, argumentsPropagateNullability: TrueArrays[1], method.ReturnType, @@ -153,7 +152,7 @@ public NpgsqlRangeTranslator( return _sqlExpressionFactory.Function( member.Name == nameof(NpgsqlRange.LowerBound) ? "lower" : "upper", - new[] { instance }, + [instance], nullable: true, argumentsPropagateNullability: TrueArrays[1], returnType, @@ -171,10 +170,10 @@ public NpgsqlRangeTranslator( _ => null }; - SqlFunctionExpression SingleArgBoolFunction(string name, SqlExpression argument) + SqlExpression SingleArgBoolFunction(string name, SqlExpression argument) => _sqlExpressionFactory.Function( name, - new[] { argument }, + [argument], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(bool)); diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs index 4af7bdcb8..659d27b73 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs @@ -12,10 +12,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte public class NpgsqlRegexIsMatchTranslator : IMethodCallTranslator { private static readonly MethodInfo IsMatch = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string) })!; + typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string)])!; private static readonly MethodInfo IsMatchWithRegexOptions = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string), typeof(RegexOptions) })!; + typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string), typeof(RegexOptions)])!; private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs index aa21383e7..f1e6f3aeb 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs @@ -18,7 +18,7 @@ public class NpgsqlRowValueTranslator : IMethodCallTranslator private static readonly MethodInfo GreaterThan = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( nameof(NpgsqlDbFunctionsExtensions.GreaterThan), - new[] { typeof(DbFunctions), typeof(ITuple), typeof(ITuple) })!; + [typeof(DbFunctions), typeof(ITuple), typeof(ITuple)])!; private static readonly MethodInfo LessThan = typeof(NpgsqlDbFunctionsExtensions).GetMethods() diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs index f902412b5..1bb84be83 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs @@ -63,7 +63,7 @@ public NpgsqlStatisticsAggregateMethodTranslator( { return _sqlExpressionFactory.AggregateFunction( functionName, - new[] { sqlExpression }, + [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], @@ -101,7 +101,7 @@ public NpgsqlStatisticsAggregateMethodTranslator( return method.Name == nameof(NpgsqlAggregateDbFunctionsExtensions.RegrCount) ? _sqlExpressionFactory.AggregateFunction( functionName, - new[] { y, x }, + [y, x], source, nullable: true, argumentsPropagateNullability: FalseArrays[2], @@ -109,7 +109,7 @@ public NpgsqlStatisticsAggregateMethodTranslator( _longTypeMapping) : _sqlExpressionFactory.AggregateFunction( functionName, - new[] { y, x }, + [y, x], source, nullable: true, argumentsPropagateNullability: FalseArrays[2], diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs index 90efa7cb4..a64f931b1 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs @@ -31,11 +31,11 @@ public NpgsqlStringMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) MemberInfo member, Type returnType, IDiagnosticsLogger logger) - => member.Name == nameof(string.Length) && instance?.Type == typeof(string) + => member.Name == nameof(string.Length) && member.DeclaringType == typeof(string) ? _sqlExpressionFactory.Convert( _sqlExpressionFactory.Function( "length", - new[] { instance }, + [instance!], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(long)), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs index cffe8d89f..d56f63380 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs @@ -1,5 +1,6 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; @@ -15,28 +16,28 @@ public class NpgsqlStringMethodTranslator : IMethodCallTranslator { private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly SqlConstantExpression _whitespace; + private readonly SqlExpression _whitespace; #region MethodInfo - private static readonly MethodInfo IndexOfChar = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), new[] { typeof(char) })!; - private static readonly MethodInfo IndexOfString = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), new[] { typeof(string) })!; + private static readonly MethodInfo IndexOfChar = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), [typeof(char)])!; + private static readonly MethodInfo IndexOfString = typeof(string).GetRuntimeMethod(nameof(string.IndexOf), [typeof(string)])!; private static readonly MethodInfo IsNullOrWhiteSpace = - typeof(string).GetRuntimeMethod(nameof(string.IsNullOrWhiteSpace), new[] { typeof(string) })!; + typeof(string).GetRuntimeMethod(nameof(string.IsNullOrWhiteSpace), [typeof(string)])!; - private static readonly MethodInfo PadLeft = typeof(string).GetRuntimeMethod(nameof(string.PadLeft), new[] { typeof(int) })!; + private static readonly MethodInfo PadLeft = typeof(string).GetRuntimeMethod(nameof(string.PadLeft), [typeof(int)])!; private static readonly MethodInfo PadLeftWithChar = typeof(string).GetRuntimeMethod( - nameof(string.PadLeft), new[] { typeof(int), typeof(char) })!; + nameof(string.PadLeft), [typeof(int), typeof(char)])!; - private static readonly MethodInfo PadRight = typeof(string).GetRuntimeMethod(nameof(string.PadRight), new[] { typeof(int) })!; + private static readonly MethodInfo PadRight = typeof(string).GetRuntimeMethod(nameof(string.PadRight), [typeof(int)])!; private static readonly MethodInfo PadRightWithChar = typeof(string).GetRuntimeMethod( - nameof(string.PadRight), new[] { typeof(int), typeof(char) })!; + nameof(string.PadRight), [typeof(int), typeof(char)])!; private static readonly MethodInfo Replace = typeof(string).GetRuntimeMethod( - nameof(string.Replace), new[] { typeof(string), typeof(string) })!; + nameof(string.Replace), [typeof(string), typeof(string)])!; private static readonly MethodInfo Substring = typeof(string).GetTypeInfo().GetDeclaredMethods(nameof(string.Substring)) .Single(m => m.GetParameters().Length == 1); @@ -44,38 +45,44 @@ public class NpgsqlStringMethodTranslator : IMethodCallTranslator private static readonly MethodInfo SubstringWithLength = typeof(string).GetTypeInfo().GetDeclaredMethods(nameof(string.Substring)) .Single(m => m.GetParameters().Length == 2); - private static readonly MethodInfo ToLower = typeof(string).GetRuntimeMethod(nameof(string.ToLower), Array.Empty())!; - private static readonly MethodInfo ToUpper = typeof(string).GetRuntimeMethod(nameof(string.ToUpper), Array.Empty())!; + private static readonly MethodInfo ToLower = typeof(string).GetRuntimeMethod(nameof(string.ToLower), [])!; + private static readonly MethodInfo ToUpper = typeof(string).GetRuntimeMethod(nameof(string.ToUpper), [])!; private static readonly MethodInfo TrimBothWithNoParam = typeof(string).GetRuntimeMethod(nameof(string.Trim), Type.EmptyTypes)!; - private static readonly MethodInfo TrimBothWithChars = typeof(string).GetRuntimeMethod(nameof(string.Trim), new[] { typeof(char[]) })!; + private static readonly MethodInfo TrimBothWithChars = typeof(string).GetRuntimeMethod(nameof(string.Trim), [typeof(char[])])!; private static readonly MethodInfo TrimBothWithSingleChar = - typeof(string).GetRuntimeMethod(nameof(string.Trim), new[] { typeof(char) })!; + typeof(string).GetRuntimeMethod(nameof(string.Trim), [typeof(char)])!; private static readonly MethodInfo TrimEndWithNoParam = typeof(string).GetRuntimeMethod(nameof(string.TrimEnd), Type.EmptyTypes)!; private static readonly MethodInfo TrimEndWithChars = typeof(string).GetRuntimeMethod( - nameof(string.TrimEnd), new[] { typeof(char[]) })!; + nameof(string.TrimEnd), [typeof(char[])])!; private static readonly MethodInfo TrimEndWithSingleChar = - typeof(string).GetRuntimeMethod(nameof(string.TrimEnd), new[] { typeof(char) })!; + typeof(string).GetRuntimeMethod(nameof(string.TrimEnd), [typeof(char)])!; private static readonly MethodInfo TrimStartWithNoParam = typeof(string).GetRuntimeMethod(nameof(string.TrimStart), Type.EmptyTypes)!; private static readonly MethodInfo TrimStartWithChars = - typeof(string).GetRuntimeMethod(nameof(string.TrimStart), new[] { typeof(char[]) })!; + typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char[])])!; private static readonly MethodInfo TrimStartWithSingleChar = - typeof(string).GetRuntimeMethod(nameof(string.TrimStart), new[] { typeof(char) })!; + typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char)])!; private static readonly MethodInfo Reverse = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Reverse), new[] { typeof(DbFunctions), typeof(string) })!; + nameof(NpgsqlDbFunctionsExtensions.Reverse), [typeof(DbFunctions), typeof(string)])!; private static readonly MethodInfo StringToArray = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.StringToArray), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; + nameof(NpgsqlDbFunctionsExtensions.StringToArray), [typeof(DbFunctions), typeof(string), typeof(string)])!; private static readonly MethodInfo StringToArrayNullString = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.StringToArray), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) })!; + nameof(NpgsqlDbFunctionsExtensions.StringToArray), [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; + + private static readonly MethodInfo ToDate = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( + nameof(NpgsqlDbFunctionsExtensions.ToDate), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo ToTimestamp = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( + nameof(NpgsqlDbFunctionsExtensions.ToTimestamp), [typeof(DbFunctions), typeof(string), typeof(string)])!; private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs = typeof(Enumerable).GetRuntimeMethods().Single( @@ -89,16 +96,19 @@ private static readonly MethodInfo LastOrDefaultMethodInfoWithoutArgs // ReSharper disable InconsistentNaming private static readonly MethodInfo String_Join1 = - typeof(string).GetMethod(nameof(string.Join), new[] { typeof(string), typeof(object[]) })!; + typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(object[])])!; private static readonly MethodInfo String_Join2 = - typeof(string).GetMethod(nameof(string.Join), new[] { typeof(string), typeof(string[]) })!; + typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(string[])])!; private static readonly MethodInfo String_Join3 = - typeof(string).GetMethod(nameof(string.Join), new[] { typeof(char), typeof(object[]) })!; + typeof(string).GetMethod(nameof(string.Join), [typeof(char), typeof(object[])])!; private static readonly MethodInfo String_Join4 = - typeof(string).GetMethod(nameof(string.Join), new[] { typeof(char), typeof(string[]) })!; + typeof(string).GetMethod(nameof(string.Join), [typeof(char), typeof(string[])])!; + + private static readonly MethodInfo String_Join5 = + typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(IEnumerable)])!; private static readonly MethodInfo String_Join_generic1 = typeof(string).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) @@ -159,7 +169,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I var argument = arguments[0]; return _sqlExpressionFactory.Function( "substr", - new[] { argument, _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1) }, + [argument, _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1)], nullable: true, argumentsPropagateNullability: TrueArrays[3], method.ReturnType); @@ -170,17 +180,16 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I var argument = arguments[0]; return _sqlExpressionFactory.Function( "substr", - new[] - { + [ argument, _sqlExpressionFactory.Function( "length", - new[] { argument }, + [argument], nullable: true, - argumentsPropagateNullability: new[] { true }, + argumentsPropagateNullability: [true], typeof(int)), _sqlExpressionFactory.Constant(1) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[3], method.ReturnType); @@ -199,11 +208,10 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I return _sqlExpressionFactory.Subtract( _sqlExpressionFactory.Function( "strpos", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(argument, stringTypeMapping) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[2], method.ReturnType), @@ -218,12 +226,11 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I return _sqlExpressionFactory.Function( "replace", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(oldValue, stringTypeMapping), _sqlExpressionFactory.ApplyTypeMapping(newValue, stringTypeMapping) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[3], method.ReturnType, @@ -234,7 +241,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I { return _sqlExpressionFactory.Function( method == ToLower ? "lower" : "upper", - new[] { instance! }, + [instance!], nullable: true, argumentsPropagateNullability: TrueArrays[1], method.ReturnType, @@ -245,7 +252,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I { var args = method == Substring - ? new[] { instance!, GenerateOneBasedIndexExpression(arguments[0]) } + ? [instance!, GenerateOneBasedIndexExpression(arguments[0])] : new[] { instance!, GenerateOneBasedIndexExpression(arguments[0]), arguments[1] }; return _sqlExpressionFactory.Function( "substring", @@ -265,7 +272,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I _sqlExpressionFactory.Equal( _sqlExpressionFactory.Function( "btrim", - new[] { argument, _whitespace }, + [argument, _whitespace], nullable: true, argumentsPropagateNullability: TrueArrays[2], argument.Type, @@ -294,19 +301,18 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I } trimChars = constantTrimChars.Value is char c - ? new[] { c } + ? [c] : (char[]?)constantTrimChars.Value; } return _sqlExpressionFactory.Function( isTrimStart ? "ltrim" : isTrimEnd ? "rtrim" : "btrim", - new[] - { + [ instance!, trimChars is null || trimChars.Length == 0 ? _whitespace : _sqlExpressionFactory.Constant(new string(trimChars)) - }, + ], nullable: true, argumentsPropagateNullability: TrueArrays[2], instance!.Type, @@ -317,7 +323,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I { var args = method == PadLeft || method == PadRight - ? new[] { instance!, arguments[0] } + ? [instance!, arguments[0]] : new[] { instance!, arguments[0], arguments[1] }; return _sqlExpressionFactory.Function( @@ -334,8 +340,10 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I || method == String_Join2 || method == String_Join3 || method == String_Join4 + || method == String_Join5 || method.IsClosedFormOf(String_Join_generic1) - || method.IsClosedFormOf(String_Join_generic2))) + || method.IsClosedFormOf(String_Join_generic2)) + && arguments[1].TypeMapping is NpgsqlArrayTypeMapping) { // If the array of strings to be joined is a constant (NewArrayExpression), we translate to concat_ws. // Otherwise we translate to array_to_string, which also supports array columns and parameters. @@ -373,7 +381,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I return _sqlExpressionFactory.Function( "array_to_string", - new[] { arguments[1], arguments[0], _sqlExpressionFactory.Constant("") }, + [arguments[1], arguments[0], _sqlExpressionFactory.Constant("")], nullable: true, argumentsPropagateNullability: TrueArrays[3], typeof(string)); @@ -388,7 +396,7 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I { return _sqlExpressionFactory.Function( "reverse", - new[] { arguments[1] }, + [arguments[1]], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(string), @@ -400,9 +408,9 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I // Note that string_to_array always returns text[], regardless of the input type return _sqlExpressionFactory.Function( "string_to_array", - new[] { arguments[1], arguments[2] }, + [arguments[1], arguments[2]], nullable: true, - argumentsPropagateNullability: new[] { true, false }, + argumentsPropagateNullability: [true, false], typeof(string[]), _typeMappingSource.FindMapping(typeof(string[]))); } @@ -412,13 +420,37 @@ public NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, I // Note that string_to_array always returns text[], regardless of the input type return _sqlExpressionFactory.Function( "string_to_array", - new[] { arguments[1], arguments[2], arguments[3] }, + [arguments[1], arguments[2], arguments[3]], nullable: true, - argumentsPropagateNullability: new[] { true, false, false }, + argumentsPropagateNullability: [true, false, false], typeof(string[]), _typeMappingSource.FindMapping(typeof(string[]))); } + if (method == ToDate) + { + return _sqlExpressionFactory.Function( + "to_date", + [arguments[1], arguments[2]], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(DateOnly), + _typeMappingSource.FindMapping(typeof(DateOnly)) + ); + } + + if (method == ToTimestamp) + { + return _sqlExpressionFactory.Function( + "to_timestamp", + [arguments[1], arguments[2]], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(DateTime), + _typeMappingSource.FindMapping(typeof(DateTime)) + ); + } + return null; } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs index 8496ab69c..a191699f2 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs @@ -23,7 +23,7 @@ public NpgsqlTimeSpanMemberTranslator(ISqlExpressionFactory sqlExpressionFactory _sqlExpressionFactory = sqlExpressionFactory; } - private static readonly bool[] FalseTrueArray = { false, true }; + private static readonly bool[] FalseTrueArray = [false, true]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -68,20 +68,20 @@ SqlExpression Floor(SqlExpression value) => _sqlExpressionFactory.Convert( _sqlExpressionFactory.Function( "floor", - new[] { value }, + [value], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(double)), typeof(int)); - SqlFunctionExpression DatePart(string part, SqlExpression value) + SqlExpression DatePart(string part, SqlExpression value) => _sqlExpressionFactory.Function( - "date_part", new[] { _sqlExpressionFactory.Constant(part), value }, + "date_part", [_sqlExpressionFactory.Constant(part), value], nullable: true, argumentsPropagateNullability: FalseTrueArray, returnType); - SqlBinaryExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) + SqlExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) => _sqlExpressionFactory.Divide(DatePart("epoch", instance), _sqlExpressionFactory.Constant(divisor)); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTrigramsMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTrigramsMethodTranslator.cs index 647d9ee92..291dcd3d4 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTrigramsMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTrigramsMethodTranslator.cs @@ -57,7 +57,7 @@ private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters private readonly RelationalTypeMapping _boolMapping; private readonly RelationalTypeMapping _floatMapping; - private static readonly bool[][] TrueArrays = { Array.Empty(), new[] { true }, new[] { true, true } }; + private static readonly bool[][] TrueArrays = [[], [true], [true, true]]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs index 714b895ac..0ee27abd3 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs @@ -8,6 +8,8 @@ /// public class PgAllExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -63,6 +65,16 @@ public virtual PgAllExpression Update(SqlExpression item, SqlExpression array) ? new PgAllExpression(item, array, OperatorType, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgAllExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAllOperatorType), typeof(RelationalTypeMapping)])!, + Item.Quote(), + Array.Quote(), + Constant(OperatorType), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Item), (SqlExpression)visitor.Visit(Array)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs index 94c2330dd..40c732644 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs @@ -11,6 +11,8 @@ /// public class PgAnyExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -74,6 +76,16 @@ public virtual PgAnyExpression Update(SqlExpression item, SqlExpression array) ? new PgAnyExpression(item, array, OperatorType, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgAnyExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAnyOperatorType), typeof(RelationalTypeMapping)])!, + Item.Quote(), + Array.Quote(), + Constant(OperatorType), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Item), (SqlExpression)visitor.Visit(Array)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs index 4a4f3d51b..d3e676fb7 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs @@ -9,6 +9,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgArrayIndexExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The array being indexed. /// @@ -75,6 +77,17 @@ public virtual PgArrayIndexExpression Update(SqlExpression array, SqlExpression ? this : new PgArrayIndexExpression(array, index, IsNullable, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgArrayIndexExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Array.Quote(), + Index.Quote(), + Constant(IsNullable), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Array), (SqlExpression)visitor.Visit(Index)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs index 3d03488f1..c8a749402 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs @@ -8,6 +8,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgArraySliceExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The array being sliced. /// @@ -72,6 +74,18 @@ public virtual PgArraySliceExpression Update(SqlExpression array, SqlExpression? ? this : new PgArraySliceExpression(array, lowerBound, upperBound, IsNullable, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgArraySliceExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(SqlExpression), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Array.Quote(), + RelationalExpressionQuotingUtilities.QuoteOrNull(LowerBound), + RelationalExpressionQuotingUtilities.QuoteOrNull(UpperBound), + Constant(IsNullable), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update( diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs index 0cbee2c1e..03387b988 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs @@ -7,6 +7,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgBinaryExpression : SqlExpression { + private static ConstructorInfo? _quotingConstructor; + /// /// Creates a new instance of the class. /// @@ -74,6 +76,17 @@ public virtual PgBinaryExpression Update(SqlExpression left, SqlExpression right : this; } + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgBinaryExpression).GetConstructor( + [typeof(PgExpressionType), typeof(SqlExpression), typeof(SqlExpression), typeof(Type), typeof(RelationalTypeMapping)])!, + Constant(OperatorType), + Left.Quote(), + Right.Quote(), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs index c129ad530..8b98e44e9 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs @@ -6,7 +6,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// Represents a SQL function call expression, supporting PostgreSQL's named parameter notation /// (e.g. make_interval(weeks => 2) and non-comma parameter separators (e.g. position(substring in string)). /// -[DebuggerDisplay("{" + nameof(ToString) + "()}")] public class PgFunctionExpression : SqlFunctionExpression, IEquatable { /// @@ -63,7 +62,7 @@ public static PgFunctionExpression CreateWithNamedArguments( return new PgFunctionExpression( name, arguments, argumentNames, argumentSeparators: null, - aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: Array.Empty(), + aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); } @@ -85,7 +84,7 @@ public static PgFunctionExpression CreateWithArgumentSeparators( return new PgFunctionExpression( name, arguments, argumentNames: null, argumentSeparators: argumentSeparators, - aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: Array.Empty(), + aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); } @@ -109,8 +108,8 @@ public PgFunctionExpression( Check.NotEmpty(name, nameof(name)); Check.NotNull(type, nameof(type)); - ArgumentNames = (argumentNames ?? Array.Empty()).ToList(); - ArgumentSeparators = (argumentSeparators ?? Array.Empty()).ToList(); + ArgumentNames = (argumentNames ?? []).ToList(); + ArgumentSeparators = (argumentSeparators ?? []).ToList(); if (ArgumentNames.SkipWhile(a => a is null).Contains(null)) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs index 850b1f358..9b816886d 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs @@ -6,6 +6,8 @@ // ReSharper disable once InconsistentNaming public class PgILikeExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The match expression. /// @@ -60,6 +62,16 @@ public virtual PgILikeExpression Update( ? this : new PgILikeExpression(match, pattern, escapeChar, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgILikeExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(SqlExpression), typeof(RelationalTypeMapping)])!, + Match.Quote(), + Pattern.Quote(), + RelationalExpressionQuotingUtilities.QuoteOrNull(EscapeChar), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public override bool Equals(object? obj) => obj is PgILikeExpression other && Equals(other); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs index 076c8caea..61871b567 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs @@ -5,6 +5,8 @@ /// public class PgJsonTraversalExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The match expression. /// @@ -56,6 +58,17 @@ public virtual PgJsonTraversalExpression Update(SqlExpression expression, IReadO ? this : new PgJsonTraversalExpression(expression, path, ReturnsText, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgJsonTraversalExpression).GetConstructor( + [typeof(SqlExpression), typeof(IReadOnlyList), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Expression.Quote(), + NewArrayInit(typeof(SqlExpression), initializers: Path.Select(a => a.Quote())), + Constant(ReturnsText), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// /// Appends an additional path component to this and returns the result. /// diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs index 5d0c56412..22cfe208d 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs @@ -5,6 +5,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgNewArrayExpression : SqlExpression { + private static ConstructorInfo? _quotingConstructor; + /// /// Creates a new instance of the class. /// @@ -44,7 +46,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) var visitedExpression = (SqlExpression)visitor.Visit(expression); if (visitedExpression != expression && newExpressions is null) { - newExpressions = new List(); + newExpressions = []; for (var j = 0; j < i; j++) { newExpressions.Add(Expressions[j]); @@ -74,6 +76,15 @@ public virtual PgNewArrayExpression Update(IReadOnlyList expressi : new PgNewArrayExpression(expressions, Type, TypeMapping); } + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgNewArrayExpression).GetConstructor( + [typeof(IReadOnlyList), typeof(Type), typeof(RelationalTypeMapping)])!, + NewArrayInit(typeof(SqlExpression), initializers: Expressions.Select(a => a.Quote())), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs index fa2a1a34b..f907e7b42 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs @@ -7,6 +7,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgRegexMatchExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -58,6 +60,16 @@ public virtual PgRegexMatchExpression Update(SqlExpression match, SqlExpression ? new PgRegexMatchExpression(match, pattern, Options, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgRegexMatchExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(RegexOptions), typeof(RelationalTypeMapping)])!, + Match.Quote(), + Pattern.Quote(), + Constant(Options), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public virtual bool Equals(PgRegexMatchExpression? other) => ReferenceEquals(this, other) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs index 19d8c6b22..0d0e6f305 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs @@ -11,13 +11,18 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgRowValueExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The values of this PostgreSQL row value expression. /// public virtual IReadOnlyList Values { get; } /// - public PgRowValueExpression(IReadOnlyList values, Type type, RelationalTypeMapping? typeMapping = null) + public PgRowValueExpression( + IReadOnlyList values, + Type type, + RelationalTypeMapping? typeMapping = null) : base(type, typeMapping) { Check.NotNull(values, nameof(values)); @@ -64,6 +69,15 @@ public virtual PgRowValueExpression Update(IReadOnlyList values) ? this : new PgRowValueExpression(values, Type); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgRowValueExpression).GetConstructor( + [typeof(IReadOnlyList), typeof(Type), typeof(RelationalTypeMapping)])!, + NewArrayInit(typeof(SqlExpression), initializers: Values.Select(a => a.Quote())), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs index 360e06da8..0d7f5c418 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs @@ -76,9 +76,34 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override PgTableValuedFunctionExpression Update(IReadOnlyList arguments) - => !arguments.SequenceEqual(Arguments) - ? new PgTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality) - : this; + => arguments.SequenceEqual(Arguments, ReferenceEqualityComparer.Instance) + ? this + : new PgTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality); + + /// + public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor) + { + var arguments = new SqlExpression[Arguments.Count]; + for (var i = 0; i < arguments.Length; i++) + { + arguments[i] = (SqlExpression)cloningExpressionVisitor.Visit(Arguments[i]); + } + + return new PgTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality); + } + + /// + public override PgTableValuedFunctionExpression WithAlias(string newAlias) + => new(newAlias, Name, Arguments, ColumnInfos, WithOrdinality); + + /// + /// 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 PgTableValuedFunctionExpression WithColumnInfos(IReadOnlyList columnInfos) + => new(Alias, Name, Arguments, columnInfos, WithOrdinality); /// protected override void Print(ExpressionPrinter expressionPrinter) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs index bf2b89126..2d8dd0ee7 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs @@ -9,6 +9,8 @@ /// public class PgUnknownBinaryExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The left-hand expression. /// @@ -59,6 +61,17 @@ public virtual PgUnknownBinaryExpression Update(SqlExpression left, SqlExpressio ? this : new PgUnknownBinaryExpression(left, right, Operator, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgUnknownBinaryExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(string), typeof(Type), typeof(RelationalTypeMapping)])!, + Left.Quote(), + Right.Quote(), + Constant(Operator), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public virtual bool Equals(PgUnknownBinaryExpression? other) => ReferenceEquals(this, other) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs index e27f49d8e..5d1a6b31a 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs @@ -54,7 +54,12 @@ public virtual string ColumnName /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public PgUnnestExpression(string alias, SqlExpression array, string columnName, bool withOrdinality = true) - : base(alias, "unnest", new[] { array }, new[] { new ColumnInfo(columnName) }, withOrdinality) + : this(alias, array, new ColumnInfo(columnName), withOrdinality) + { + } + + private PgUnnestExpression(string alias, SqlExpression array, ColumnInfo? columnInfo, bool withOrdinality = true) + : base(alias, "unnest", [array], columnInfo is null ? null : [columnInfo.Value], withOrdinality) { } @@ -85,4 +90,25 @@ public virtual PgUnnestExpression Update(SqlExpression array) => array == Array ? this : new PgUnnestExpression(Alias, array, ColumnName, WithOrdinality); + + /// + public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor) + => new PgUnnestExpression(alias!, (SqlExpression)cloningExpressionVisitor.Visit(Array), ColumnName, WithOrdinality); + + /// + public override PgUnnestExpression WithAlias(string newAlias) + => new(newAlias, Array, ColumnName, WithOrdinality); + + /// + public override PgUnnestExpression WithColumnInfos(IReadOnlyList columnInfos) + => new( + Alias, + Array, + columnInfos switch + { + [] => null, + [var columnInfo] => columnInfo, + _ => throw new ArgumentException() + }, + WithOrdinality); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs index c8d826357..2b5731698 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs @@ -34,18 +34,12 @@ public override object GenerateCacheKey(Expression query, bool async) GenerateCacheKeyCore(query, async), RelationalDependencies.ContextOptions.FindExtension()?.ReverseNullOrdering ?? false); - private struct NpgsqlCompiledQueryCacheKey + private struct NpgsqlCompiledQueryCacheKey( + RelationalCompiledQueryCacheKey relationalCompiledQueryCacheKey, + bool reverseNullOrdering) { - private readonly RelationalCompiledQueryCacheKey _relationalCompiledQueryCacheKey; - private readonly bool _reverseNullOrdering; - - public NpgsqlCompiledQueryCacheKey( - RelationalCompiledQueryCacheKey relationalCompiledQueryCacheKey, - bool reverseNullOrdering) - { - _relationalCompiledQueryCacheKey = relationalCompiledQueryCacheKey; - _reverseNullOrdering = reverseNullOrdering; - } + private readonly RelationalCompiledQueryCacheKey _relationalCompiledQueryCacheKey = relationalCompiledQueryCacheKey; + private readonly bool _reverseNullOrdering = reverseNullOrdering; public override bool Equals(object? obj) => !(obj is null) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs index cf7a4f7b5..bba009ead 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs @@ -41,7 +41,7 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) { throw new InvalidOperationException( RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( - nameof(RelationalQueryableExtensions.ExecuteDelete))); + nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); } var fromItems = new List(); @@ -56,7 +56,7 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) switch (tableBase) { case TableExpression tableExpression: - if (tableExpression != deleteExpression.Table) + if (tableExpression.Alias != deleteExpression.Table.Alias) { fromItems.Add(tableExpression); } @@ -64,7 +64,7 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) break; case InnerJoinExpression { Table: { } tableExpression } innerJoinExpression: - if (tableExpression != deleteExpression.Table) + if (tableExpression.Alias != deleteExpression.Table.Alias) { fromItems.Add(tableExpression); } @@ -79,7 +79,7 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) default: throw new InvalidOperationException( RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( - nameof(RelationalQueryableExtensions.ExecuteDelete))); + nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs index b03807f13..da5ebbbd1 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs @@ -11,10 +11,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; public class NpgsqlEvaluatableExpressionFilter : RelationalEvaluatableExpressionFilter { private static readonly MethodInfo TsQueryParse = - typeof(NpgsqlTsQuery).GetRuntimeMethod(nameof(NpgsqlTsQuery.Parse), new[] { typeof(string) })!; + typeof(NpgsqlTsQuery).GetRuntimeMethod(nameof(NpgsqlTsQuery.Parse), [typeof(string)])!; private static readonly MethodInfo TsVectorParse = - typeof(NpgsqlTsVector).GetRuntimeMethod(nameof(NpgsqlTsVector.Parse), new[] { typeof(string) })!; + typeof(NpgsqlTsVector).GetRuntimeMethod(nameof(NpgsqlTsVector.Parse), [typeof(string)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs index c79480a72..dbb97a5c4 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs @@ -16,8 +16,8 @@ public class NpgsqlParameterBasedSqlProcessor : RelationalParameterBasedSqlProce /// public NpgsqlParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } @@ -48,7 +48,7 @@ protected override Expression ProcessSqlNullability( Check.NotNull(selectExpression, nameof(selectExpression)); Check.NotNull(parametersValues, nameof(parametersValues)); - return new NpgsqlSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process( + return new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process( selectExpression, parametersValues, out canCache); } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs index d3d0d342d..f4cf99980 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs @@ -23,11 +23,10 @@ public NpgsqlParameterBasedSqlProcessorFactory( } /// - /// 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. + /// Creates a new . /// - public virtual RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) - => new NpgsqlParameterBasedSqlProcessor(_dependencies, useRelationalNulls); + /// Parameters for . + /// A relational parameter based sql processor. + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new NpgsqlParameterBasedSqlProcessor(_dependencies, parameters); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs index 517462731..259cd1ec7 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs @@ -18,7 +18,25 @@ public NpgsqlQueryCompilationContext( QueryCompilationContextDependencies dependencies, RelationalQueryCompilationContextDependencies relationalDependencies, bool async) - : base(dependencies, relationalDependencies, async) + : this( + dependencies, relationalDependencies, async, precompiling: false, + nonNullableReferenceTypeParameters: 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. + /// + public NpgsqlQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async, + bool precompiling, + IReadOnlySet? nonNullableReferenceTypeParameters) + : base(dependencies, relationalDependencies, async, precompiling, nonNullableReferenceTypeParameters) { } @@ -30,4 +48,7 @@ public NpgsqlQueryCompilationContext( /// public override bool IsBuffering => base.IsBuffering || QuerySplittingBehavior == Microsoft.EntityFrameworkCore.QuerySplittingBehavior.SplitQuery; + + /// + public override bool SupportsPrecompiledQuery => true; } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs index f3a43623b..7f6bbfc3a 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs @@ -36,4 +36,15 @@ public NpgsqlQueryCompilationContextFactory( /// public virtual QueryCompilationContext Create(bool async) => new NpgsqlQueryCompilationContext(_dependencies, _relationalDependencies, async); + + /// + /// 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 QueryCompilationContext CreatePrecompiled(bool async, IReadOnlySet nonNullableReferenceTypeParameters) + => new NpgsqlQueryCompilationContext( + _dependencies, _relationalDependencies, async, precompiling: true, + nonNullableReferenceTypeParameters); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs deleted file mode 100644 index ade336f61..000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlQueryRootProcessor : RelationalQueryRootProcessor -{ - private readonly bool _supportsUnnest; - - /// - /// 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 NpgsqlQueryRootProcessor( - QueryTranslationPreprocessorDependencies dependencies, - RelationalQueryTranslationPreprocessorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies, queryCompilationContext) - { - _supportsUnnest = !npgsqlSingletonOptions.UseRedshift; - } - - /// - /// Converts a to a , to be later translated to - /// PostgreSQL unnest over an array parameter. - /// - /// - /// 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 override bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression) - => _supportsUnnest; -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 42e9684c3..5c1ed24f1 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -131,6 +131,12 @@ ExpressionType.Add when ExpressionType.And when e.Type == typeof(bool) => " AND ", ExpressionType.Or when e.Type == typeof(bool) => " OR ", + + // In most databases/languages, the caret (^) is the bitwise XOR operator. But in PostgreSQL the caret is the exponentiation + // operator, and hash (#) is used instead. + ExpressionType.ExclusiveOr when e.Type == typeof(bool) => " <> ", + ExpressionType.ExclusiveOr => " # ", + _ => base.GetOperator(e) }; @@ -335,7 +341,7 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression) var table = selectExpression.Tables[i]; var joinExpression = table as JoinExpressionBase; - if (ReferenceEquals(updateExpression.Table, joinExpression?.Table ?? table)) + if (updateExpression.Table.Alias == (joinExpression?.Table.Alias ?? table.Alias)) { LiftPredicate(table); continue; @@ -406,7 +412,7 @@ void LiftPredicate(TableExpressionBase joinTable) } throw new InvalidOperationException( - RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))); } /// @@ -608,11 +614,29 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio Visit(sqlUnaryExpression.Operand); return sqlUnaryExpression; - // Not operation on full-text queries + // NOT operation on full-text queries case ExpressionType.Not when sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(NpgsqlTsQuery): Sql.Append("!!"); Visit(sqlUnaryExpression.Operand); return sqlUnaryExpression; + + // NOT over expression types which have fancy embedded negation + case ExpressionType.Not + when sqlUnaryExpression.Type == typeof(bool): + { + switch (sqlUnaryExpression.Operand) + { + case PgRegexMatchExpression regexMatch: + VisitRegexMatch(regexMatch, negated: true); + return sqlUnaryExpression; + + case PgILikeExpression iLike: + VisitILike(iLike, negated: true); + return sqlUnaryExpression; + } + + break; + } } return base.VisitSqlUnary(sqlUnaryExpression); @@ -760,6 +784,16 @@ protected override Expression VisitValues(ValuesExpression valuesExpression) /// protected override void GenerateValues(ValuesExpression valuesExpression) { + if (valuesExpression.RowValues is null) + { + throw new UnreachableException(); + } + + if (valuesExpression.RowValues.Count == 0) + { + throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + } + // PostgreSQL supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b). // But since other databases sometimes don't, the default relational implementation is complex, involving a SELECT for the first row // and a UNION All on the rest. Override to do the nice simple thing. @@ -891,16 +925,12 @@ protected virtual Expression VisitArraySlice(PgArraySliceExpression expression) } /// - /// Visits the children of a . + /// Produces SQL for PostgreSQL regex matching. /// - /// The expression. - /// - /// An . - /// /// /// See: http://www.postgresql.org/docs/current/static/functions-matching.html /// - protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression) + protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, bool negated = false) { var options = expression.Options; @@ -908,12 +938,12 @@ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression) if (options.HasFlag(RegexOptions.IgnoreCase)) { - Sql.Append(" ~* "); + Sql.Append(negated ? " !~* " : " ~* "); options &= ~RegexOptions.IgnoreCase; } else { - Sql.Append(" ~ "); + Sql.Append(negated ? " !~ " : " ~ "); } // PG regexps are single-line by default @@ -957,7 +987,7 @@ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression) } else { - Sql.Append(constantPattern); + Sql.Append(constantPattern.Replace("'", "''")); Sql.Append("'"); } @@ -996,16 +1026,22 @@ protected virtual Expression VisitRowValue(PgRowValueExpression rowValueExpressi } /// - /// Visits the children of an . + /// 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 expression. - /// - /// An . - /// - protected virtual Expression VisitILike(PgILikeExpression likeExpression) + protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool negated = false) { Visit(likeExpression.Match); + + if (negated) + { + Sql.Append(" NOT"); + } + Sql.Append(" ILIKE "); + Visit(likeExpression.Pattern); if (likeExpression.EscapeChar is not null) @@ -1083,8 +1119,7 @@ void GenerateJsonPath(bool returnsText) s => s switch { { PropertyName: string propertyName } - => new SqlConstantExpression( - Expression.Constant(propertyName), _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), + => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), { ArrayIndex: SqlExpression arrayIndex } => arrayIndex, _ => throw new UnreachableException() }).ToList()); @@ -1118,7 +1153,8 @@ private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadO Sql.Append(returnsText ? " #>> " : " #> "); // Use simplified array literal syntax if all path components are constants for cleaner SQL - if (path.All(p => p is SqlConstantExpression)) + if (path.All(p => p is SqlConstantExpression { Value: var pathSegment } + && (pathSegment is not string s || s.All(char.IsAsciiLetterOrDigit)))) { Sql .Append("'{") @@ -1401,7 +1437,7 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx // If both operators have the same precedence, add parentheses unless they're the same operator, and // that operator is associative (e.g. a + b + c) - 0 => outerExpression is not PgBinaryExpression outerBinary + _ => outerExpression is not PgBinaryExpression outerBinary || outerBinary.OperatorType != innerBinary.OperatorType || !isOuterAssociative // Arithmetic operators on floating points aren't associative, because of rounding errors. @@ -1416,6 +1452,9 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx return true; } + case PgUnknownBinaryExpression: + return true; + default: return base.RequiresParentheses(outerExpression, innerExpression); } @@ -1485,7 +1524,7 @@ protected override bool TryGetOperatorInfo(SqlExpression expression, out int pre PgBinaryExpression => (1000, false), CollateExpression => (1000, false), - AtTimeZoneExpression => (1000, false), + AtTimeZoneExpression => (1100, false), InExpression => (900, false), PgJsonTraversalExpression => (1000, false), PgArrayIndexExpression => (1500, false), @@ -1516,16 +1555,10 @@ private void GenerateList( } } - private sealed class OuterReferenceFindingExpressionVisitor : ExpressionVisitor + private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor { - private readonly TableExpression _mainTable; private bool _containsReference; - public OuterReferenceFindingExpressionVisitor(TableExpression mainTable) - { - _mainTable = mainTable; - } - public bool ContainsReferenceToMainTable(SqlExpression sqlExpression) { _containsReference = false; @@ -1543,8 +1576,8 @@ public bool ContainsReferenceToMainTable(SqlExpression sqlExpression) return expression; } - if (expression is ColumnExpression columnExpression - && columnExpression.Table == _mainTable) + if (expression is ColumnExpression { TableAlias: var tableAlias } + && tableAlias == mainTable.Alias) { _containsReference = true; diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs index 7b9a9828d..0bb5e96d6 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs @@ -8,6 +8,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// public class NpgsqlQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor { + private readonly NpgsqlSqlTreePruner _pruner = new(); + /// /// 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 @@ -17,7 +19,7 @@ public class NpgsqlQueryTranslationPostprocessor : RelationalQueryTranslationPos public NpgsqlQueryTranslationPostprocessor( QueryTranslationPostprocessorDependencies dependencies, RelationalQueryTranslationPostprocessorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext) + RelationalQueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { } @@ -33,8 +35,26 @@ public override Expression Process(Expression query) var result = base.Process(query); result = new NpgsqlUnnestPostprocessor().Visit(result); - result = new NpgsqlSetOperationTypeResolutionCompensatingExpressionVisitor().Visit(result); + result = new NpgsqlSetOperationTypingInjector().Visit(result); return result; } + + /// + /// 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 override Expression ProcessTypeMappings(Expression expression) + => new NpgsqlTypeMappingPostprocessor(Dependencies, RelationalDependencies, RelationalQueryCompilationContext).Process(expression); + + /// + /// 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 override Expression Prune(Expression query) + => _pruner.Prune(query); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessorFactory.cs index 0d28d80a0..e31cc18be 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessorFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessorFactory.cs @@ -34,5 +34,8 @@ public NpgsqlQueryTranslationPostprocessorFactory( /// public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext) - => new NpgsqlQueryTranslationPostprocessor(Dependencies, RelationalDependencies, queryCompilationContext); + => new NpgsqlQueryTranslationPostprocessor( + Dependencies, + RelationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs deleted file mode 100644 index b7ccfc05d..000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor -{ - private readonly INpgsqlSingletonOptions _npgsqlSingletonOptions; - - /// - /// 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 NpgsqlQueryTranslationPreprocessor( - QueryTranslationPreprocessorDependencies dependencies, - RelationalQueryTranslationPreprocessorDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions, - QueryCompilationContext queryCompilationContext) - : base(dependencies, relationalDependencies, queryCompilationContext) - { - _npgsqlSingletonOptions = npgsqlSingletonOptions; - } - - /// - /// 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 override Expression ProcessQueryRoots(Expression expression) - => new NpgsqlQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext, _npgsqlSingletonOptions) - .Visit(expression); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs deleted file mode 100644 index c1f9e818b..000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPreprocessorFactory.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory -{ - private readonly INpgsqlSingletonOptions _npgsqlSingletonOptions; - - /// - /// 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 NpgsqlQueryTranslationPreprocessorFactory( - QueryTranslationPreprocessorDependencies dependencies, - RelationalQueryTranslationPreprocessorDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - { - Dependencies = dependencies; - RelationalDependencies = relationalDependencies; - _npgsqlSingletonOptions = npgsqlSingletonOptions; - } - - /// - /// Dependencies for this service. - /// - protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; } - - /// - /// Relational provider-specific dependencies for this service. - /// - protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { 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 virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) - => new NpgsqlQueryTranslationPreprocessor(Dependencies, RelationalDependencies, _npgsqlSingletonOptions, queryCompilationContext); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs index d4d8cd7b0..8d2bc0623 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs @@ -1,4 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using Npgsql.EntityFrameworkCore.PostgreSQL.Extensions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -15,22 +17,25 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { + private readonly RelationalQueryCompilationContext _queryCompilationContext; private readonly NpgsqlTypeMappingSource _typeMappingSource; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + private readonly bool _isRedshift; + private RelationalTypeMapping? _ordinalityTypeMapping; #region MethodInfos private static readonly MethodInfo Like2MethodInfo = typeof(DbFunctionsExtensions).GetRuntimeMethod( - nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; + nameof(DbFunctionsExtensions.Like), [typeof(DbFunctions), typeof(string), typeof(string)])!; // ReSharper disable once InconsistentNaming private static readonly MethodInfo ILike2MethodInfo = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!; + nameof(NpgsqlDbFunctionsExtensions.ILike), [typeof(DbFunctions), typeof(string), typeof(string)])!; private static readonly MethodInfo MatchesLQuery = - typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), new[] { typeof(string) })!; + typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), [typeof(string)])!; #endregion @@ -43,11 +48,14 @@ private static readonly MethodInfo ILike2MethodInfo public NpgsqlQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext) + RelationalQueryCompilationContext queryCompilationContext, + INpgsqlSingletonOptions npgsqlSingletonOptions) : base(dependencies, relationalDependencies, queryCompilationContext) { + _queryCompilationContext = queryCompilationContext; _typeMappingSource = (NpgsqlTypeMappingSource)relationalDependencies.TypeMappingSource; _sqlExpressionFactory = (NpgsqlSqlExpressionFactory)relationalDependencies.SqlExpressionFactory; + _isRedshift = npgsqlSingletonOptions.UseRedshift; } /// @@ -59,8 +67,10 @@ public NpgsqlQueryableMethodTranslatingExpressionVisitor( protected NpgsqlQueryableMethodTranslatingExpressionVisitor(NpgsqlQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor) { + _queryCompilationContext = parentVisitor._queryCompilationContext; _typeMappingSource = parentVisitor._typeMappingSource; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; + _isRedshift = parentVisitor._isRedshift; } /// @@ -78,11 +88,18 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis /// 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 ShapedQueryExpression TranslatePrimitiveCollection( + protected override ShapedQueryExpression? TranslatePrimitiveCollection( SqlExpression sqlExpression, IProperty? property, string tableAlias) { + if (_isRedshift) + { + AddTranslationErrorDetails("Redshift does not support unnest, which is required for most forms of querying of JSON arrays."); + + return null; + } + var elementClrType = sqlExpression.Type.GetSequenceType(); var elementTypeMapping = (RelationalTypeMapping?)sqlExpression.TypeMapping?.ElementTypeMapping; @@ -99,7 +116,7 @@ protected override ShapedQueryExpression TranslatePrimitiveCollection( // a special case for geometry collections, where we use SelectExpression selectExpression; -#pragma warning disable EF1001 +#pragma warning disable EF1001 // SelectExpression constructors are currently internal // TODO: Parameters have no type mapping. We can check whether the expression type is one of the NTS geometry collection types, // though in a perfect world we'd actually infer this. In other words, when the type mapping of the element is inferred further on, // we'd replace the unnest expression with ST_Dump. We could even have a special expression type which means "indeterminate, must be @@ -108,8 +125,10 @@ protected override ShapedQueryExpression TranslatePrimitiveCollection( { // TODO: For geometry collection support (not yet supported), see #2850. selectExpression = new SelectExpression( - new TableValuedFunctionExpression(tableAlias, "ST_Dump", new[] { sqlExpression }), - "geom", elementClrType, elementTypeMapping, isElementNullable); + [new TableValuedFunctionExpression(tableAlias, "ST_Dump", [sqlExpression])], + new ColumnExpression("geom", tableAlias, elementClrType.UnwrapNullableType(), elementTypeMapping, isElementNullable), + identifier: [], // TODO + _queryCompilationContext.SqlAliasManager); } else { @@ -119,18 +138,23 @@ protected override ShapedQueryExpression TranslatePrimitiveCollection( // (f above); since the table alias may get uniquified by EF, this would break queries. // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression - // Note also that with PostgreSQL unnest, the output ordering is guaranteed to be the same as the input array, so we don't need - // to add ordering like in most other providers (https://www.postgresql.org/docs/current/functions-array.html) - // We also don't need to apply any casts or typing, since PG arrays are fully typed (unlike e.g. a JSON string). + + // Note also that with PostgreSQL unnest, the output ordering is guaranteed to be the same as the input array. However, we still + // need to add an explicit ordering on the ordinality column, since once the unnest is joined into a select, its "natural" + // orderings is lost and an explicit ordering is needed again (see #3207). + var (ordinalityColumn, ordinalityComparer) = GenerateOrdinalityIdentifier(tableAlias); selectExpression = new SelectExpression( - new PgUnnestExpression(tableAlias, sqlExpression, "value"), - columnName: "value", - columnType: elementClrType, - columnTypeMapping: elementTypeMapping, - isColumnNullable: isElementNullable, - identifierColumnName: "ordinality", - identifierColumnType: typeof(int), - identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))); + [new PgUnnestExpression(tableAlias, sqlExpression, "value")], + new ColumnExpression( + "value", + tableAlias, + elementClrType.UnwrapNullableType(), + elementTypeMapping, + isElementNullable), + identifier: [(ordinalityColumn, ordinalityComparer)], + _queryCompilationContext.SqlAliasManager); + + selectExpression.AppendOrdering(new OrderingExpression(ordinalityColumn, ascending: true)); } #pragma warning restore EF1001 @@ -160,7 +184,9 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr // Calculate the table alias for the jsonb_to_recordset function based on the last named path segment // (or the JSON column name if there are none) var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null); - var tableAlias = char.ToLowerInvariant((lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name)[0]).ToString(); + var tableAlias = + _queryCompilationContext.SqlAliasManager.GenerateTableAlias( + lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name); // TODO: This relies on nested JSON columns flowing across the type mapping of the top-most containing JSON column, check this. var functionName = jsonQueryExpression.JsonColumn switch @@ -214,15 +240,15 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr // Construct the json_to_recordset around the JsonScalarExpression, and wrap it in a SelectExpression var jsonToRecordSetExpression = new PgTableValuedFunctionExpression( - tableAlias, functionName, new[] { jsonScalarExpression }, columnInfos, withOrdinality: true); + tableAlias, functionName, [jsonScalarExpression], columnInfos, withOrdinality: true); -#pragma warning disable EF1001 // Internal EF Core API usage. - var selectExpression = new SelectExpression( +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var selectExpression = CreateSelect( jsonQueryExpression, jsonToRecordSetExpression, - identifierColumnName: "ordinality", - identifierColumnType: typeof(int), - identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))!); + "ordinality", + typeof(int), + _typeMappingSource.FindMapping(typeof(int))!); #pragma warning restore EF1001 // Internal EF Core API usage. return new ShapedQueryExpression( @@ -245,18 +271,6 @@ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityT .SelectMany(t => t.GetDeclaredNavigations()); } - /// - /// 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 override Expression ApplyInferredTypeMappings( - Expression expression, - IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping?> inferredTypeMappings) - => new NpgsqlInferredTypeMappingApplier( - RelationalDependencies.Model, _typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression); - /// /// 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 @@ -265,16 +279,9 @@ protected override Expression ApplyInferredTypeMappings( /// protected override ShapedQueryExpression? TranslateAll(ShapedQueryExpression source, LambdaExpression predicate) { - if (source.QueryExpression is SelectExpression - { - Tables: [(PgUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - } + if ((source.TryExtractArray(out var array, ignoreOrderings: true) + || source.TryConvertValuesToArray(out array, ignoreOrderings: true)) + && source.QueryExpression is SelectExpression { Tables: [{ Alias: var tableAlias }] } && TranslateLambdaExpression(source, predicate) is { } translatedPredicate) { switch (translatedPredicate) @@ -287,11 +294,11 @@ protected override Expression ApplyInferredTypeMappings( Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } - when pattern.Table == sourceTable: + when pattern.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, - _sqlExpressionFactory.All(match, GetArray(sourceTable), PgAllOperatorType.Like)); + _sqlExpressionFactory.All(match, array, PgAllOperatorType.Like)); } // Pattern match for: new[] { "a", "b", "c" }.All(p => EF.Functions.Like(e.SomeText, p)), @@ -302,11 +309,11 @@ protected override Expression ApplyInferredTypeMappings( Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } - when pattern.Table == sourceTable: + when pattern.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, - _sqlExpressionFactory.All(match, GetArray(sourceTable), PgAllOperatorType.ILike)); + _sqlExpressionFactory.All(match, array, PgAllOperatorType.ILike)); } // Pattern match for: e.SomeArray.All(p => ints.Contains(p)) over non-column, @@ -316,9 +323,9 @@ protected override Expression ApplyInferredTypeMappings( Item: ColumnExpression sourceColumn, Array: var otherArray } - when sourceColumn.Table == sourceTable: + when sourceColumn.TableAlias == tableAlias: { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(GetArray(sourceTable), otherArray)); + return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(array, otherArray)); } // Pattern match for: new[] { 4, 5 }.All(p => e.SomeArray.Contains(p)) over column, @@ -329,9 +336,9 @@ protected override Expression ApplyInferredTypeMappings( Left: var otherArray, Right: PgNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } } - when sourceColumn.Table == sourceTable: + when sourceColumn.TableAlias == tableAlias: { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(GetArray(sourceTable), otherArray)); + return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(array, otherArray)); } } } @@ -347,16 +354,9 @@ protected override Expression ApplyInferredTypeMappings( /// protected override ShapedQueryExpression? TranslateAny(ShapedQueryExpression source, LambdaExpression? predicate) { - if (source.QueryExpression is SelectExpression - { - Tables: [(PgUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - }) + if ((source.TryExtractArray(out var array, ignoreOrderings: true) + || source.TryConvertValuesToArray(out array, ignoreOrderings: true)) + && source.QueryExpression is SelectExpression { Tables: [{ Alias: var tableAlias }] }) { // Pattern match: x.Array.Any() // Translation: cardinality(x.array) > 0 instead of EXISTS (SELECT 1 FROM FROM unnest(x.Array)) @@ -367,7 +367,7 @@ protected override Expression ApplyInferredTypeMappings( _sqlExpressionFactory.GreaterThan( _sqlExpressionFactory.Function( "cardinality", - new[] { GetArray(sourceTable) }, + [array], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)), @@ -389,10 +389,10 @@ protected override Expression ApplyInferredTypeMappings( Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } - when pattern.Table == sourceTable: + when pattern.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( - source, _sqlExpressionFactory.Any(match, GetArray(sourceTable), PgAnyOperatorType.Like)); + source, _sqlExpressionFactory.Any(match, array, PgAnyOperatorType.Like)); } // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)) @@ -403,10 +403,10 @@ protected override Expression ApplyInferredTypeMappings( Pattern: ColumnExpression pattern, EscapeChar: SqlConstantExpression { Value: "" } } - when pattern.Table == sourceTable: + when pattern.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( - source, _sqlExpressionFactory.Any(match, GetArray(sourceTable), PgAnyOperatorType.ILike)); + source, _sqlExpressionFactory.Any(match, array, PgAnyOperatorType.ILike)); } // Array overlap over non-column @@ -417,9 +417,9 @@ protected override Expression ApplyInferredTypeMappings( Item: ColumnExpression sourceColumn, Array: var otherArray } - when sourceColumn.Table == sourceTable: + when sourceColumn.TableAlias == tableAlias: { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(GetArray(sourceTable), otherArray)); + return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array, otherArray)); } // Array overlap over column @@ -431,9 +431,9 @@ protected override Expression ApplyInferredTypeMappings( Left: var otherArray, Right: PgNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } } - when sourceColumn.Table == sourceTable: + when sourceColumn.TableAlias == tableAlias: { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(GetArray(sourceTable), otherArray)); + return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array, otherArray)); } #region LTree translations @@ -446,14 +446,14 @@ protected override Expression ApplyInferredTypeMappings( Left: var ltree, Right: SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression lqueryColumn } } - when lqueryColumn.Table == sourceTable: + when lqueryColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, new PgBinaryExpression( PgExpressionType.LTreeMatchesAny, ltree, - _sqlExpressionFactory.ApplyTypeMapping(GetArray(sourceTable), _typeMappingSource.FindMapping("lquery[]")), + _sqlExpressionFactory.ApplyTypeMapping(array, _typeMappingSource.FindMapping("lquery[]")), typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); } @@ -469,13 +469,13 @@ protected override Expression ApplyInferredTypeMappings( // Contains/ContainedBy can happen for non-LTree types too, so check that Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree } - when ltreeColumn.Table == sourceTable: + when ltreeColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, new PgBinaryExpression( operatorType, - _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), + _sqlExpressionFactory.ApplyDefaultTypeMapping(array), ltree, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); @@ -491,13 +491,13 @@ protected override Expression ApplyInferredTypeMappings( Left: ColumnExpression ltreeColumn, Right: var lquery } - when ltreeColumn.Table == sourceTable: + when ltreeColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, new PgBinaryExpression( PgExpressionType.LTreeMatches, - _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), + _sqlExpressionFactory.ApplyDefaultTypeMapping(array), lquery, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); @@ -512,13 +512,13 @@ protected override Expression ApplyInferredTypeMappings( Left: ColumnExpression ltreeColumn, Right: var lqueries } - when ltreeColumn.Table == sourceTable: + when ltreeColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, new PgBinaryExpression( PgExpressionType.LTreeMatchesAny, - _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), + _sqlExpressionFactory.ApplyDefaultTypeMapping(array), lqueries, typeof(bool), typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); @@ -540,6 +540,7 @@ protected override Expression ApplyInferredTypeMappings( Source1: { Tables: [PgUnnestExpression { Array: var array1 }], + Predicate: null, GroupBy: [], Having: null, IsDistinct: false, @@ -549,6 +550,7 @@ protected override Expression ApplyInferredTypeMappings( Source2: { Tables: [PgUnnestExpression { Array: var array2 }], + Predicate: null, GroupBy: [], Having: null, IsDistinct: false, @@ -578,26 +580,11 @@ protected override Expression ApplyInferredTypeMappings( /// protected override ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item) { - if (source.QueryExpression is SelectExpression - { - Tables: [(PgUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - }) + // Note that most other simplifications convert ValuesExpression to unnest over array constructor, but we avoid doing that + // here for Contains, since the relational translation for ValuesExpression is better. + if (source.TryExtractArray(out var array, ignoreOrderings: true) + && TranslateExpression(item, applyDefaultTypeMapping: false) is SqlExpression translatedItem) { - if (TranslateExpression(item, applyDefaultTypeMapping: false) is not SqlExpression translatedItem) - { - return null; - } - - // Note that most other simplifications here convert ValuesExpression to unnest over array constructor, but we avoid doing that - // here, since the relational translation for ValuesExpression is better. - var array = GetArray(sourceTable); - (translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array); // When the array is a column, we translate Contains to array @> ARRAY[item]. GIN indexes on array are used, but null @@ -616,7 +603,7 @@ protected override Expression ApplyInferredTypeMappings( _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function( "array_position", - new[] { array, translatedItem }, + [array, translatedItem], nullable: true, argumentsPropagateNullability: FalseArrays[2], typeof(int)))); @@ -626,7 +613,7 @@ protected override Expression ApplyInferredTypeMappings( source, _sqlExpressionFactory.Contains( array, - _sqlExpressionFactory.NewArrayOrConstant(new[] { translatedItem }, array.Type, array.TypeMapping))); + _sqlExpressionFactory.NewArrayOrConstant([translatedItem], array.Type, array.TypeMapping))); // For constant arrays (new[] { 1, 2, 3 }) or inline arrays (new[] { 1, param, 3 }), don't do anything PG-specific for since // the general EF Core mechanism is fine for that case: item IN (1, 2, 3). @@ -667,29 +654,22 @@ protected override Expression ApplyInferredTypeMappings( protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) { // Simplify x.Array.Count() => cardinality(x.Array) instead of SELECT COUNT(*) FROM unnest(x.Array) - if (predicate is null - && source.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array }], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - }) + if (predicate is null && source.TryExtractArray(out var array, ignoreOrderings: true)) { var translation = _sqlExpressionFactory.Function( "cardinality", - new[] { array }, + [array], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)); +#pragma warning disable EF1001 // SelectExpression constructors are currently internal return source.Update( - _sqlExpressionFactory.Select(translation), + new SelectExpression(translation, _queryCompilationContext.SqlAliasManager), Expression.Convert( new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), typeof(int))); +#pragma warning restore EF1001 } return base.TranslateCount(source, predicate); @@ -705,28 +685,8 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s { // Simplify x.Array.Concat(y.Array) => x.Array || y.Array instead of: // SELECT u.value FROM unnest(x.Array) UNION ALL SELECT u.value FROM unnest(y.Array) - if (source1.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array1 } unnestExpression1], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null, - Orderings: [] - } - && source2.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array2 }], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null, - Orderings: [] - } - && TryGetProjectedColumn(source1, out var projectedColumn1) - && TryGetProjectedColumn(source2, out var projectedColumn2)) + if (source1.TryExtractArray(out var array1, out var projectedColumn1) + && source2.TryExtractArray(out var array2, out var projectedColumn2)) { Check.DebugAssert(projectedColumn1.Type == projectedColumn2.Type, "projectedColumn1.Type == projectedColumn2.Type"); Check.DebugAssert( @@ -736,16 +696,13 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s // TODO: Conflicting type mappings from both sides? var inferredTypeMapping = projectedColumn1.TypeMapping ?? projectedColumn2.TypeMapping; -#pragma warning disable EF1001 // Internal EF Core API usage. +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source1.QueryExpression).Tables.Single().Alias!; var selectExpression = new SelectExpression( - new PgUnnestExpression(unnestExpression1.Alias, _sqlExpressionFactory.Add(array1, array2), "value"), - columnName: "value", - columnType: projectedColumn1.Type, - columnTypeMapping: inferredTypeMapping, - isColumnNullable: projectedColumn1.IsNullable || projectedColumn2.IsNullable, - identifierColumnName: "ordinality", - identifierColumnType: typeof(int), - identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))); + [new PgUnnestExpression(tableAlias, _sqlExpressionFactory.Add(array1, array2), "value")], + new ColumnExpression("value", tableAlias, projectedColumn1.Type, inferredTypeMapping, projectedColumn1.IsNullable || projectedColumn2.IsNullable), + identifier: [GenerateOrdinalityIdentifier(tableAlias)], + _queryCompilationContext.SqlAliasManager); #pragma warning restore EF1001 // Internal EF Core API usage. // TODO: Simplify by using UpdateQueryExpression after https://github.com/dotnet/efcore/issues/31511 @@ -781,26 +738,18 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s // Simplify x.Array[1] => x.Array[1] (using the PG array subscript operator) instead of a subquery with LIMIT/OFFSET // Note that we have unnest over multiranges, not just arrays - but multiranges don't support subscripting/slicing. if (!returnDefault - && source.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array }], - GroupBy: [], - Having: null, - IsDistinct: false, - Orderings: [], - Limit: null, - Offset: null - } - && IsPostgresArray(array) - && TryGetProjectedColumn(source, out var projectedColumn) + && source.TryExtractArray(out var array, out var projectedColumn) && TranslateExpression(index) is { } translatedIndex) { // Note that PostgreSQL arrays are 1-based, so adjust the index. +#pragma warning disable EF1001 // SelectExpression constructors are currently internal return source.UpdateQueryExpression( - _sqlExpressionFactory.Select( + new SelectExpression( _sqlExpressionFactory.ArrayIndex( array, - GenerateOneBasedIndexExpression(translatedIndex), projectedColumn.IsNullable))); + GenerateOneBasedIndexExpression(translatedIndex), projectedColumn.IsNullable), + _queryCompilationContext.SqlAliasManager)); +#pragma warning restore EF1001 } return base.TranslateElementAtOrDefault(source, index, returnDefault); @@ -821,17 +770,9 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s // Some LTree translations (see LTreeQueryTest) // Note that preprocessing normalizes FirstOrDefault(predicate) to Where(predicate).FirstOrDefault(), so the source's // select expression should already contain our predicate. - if (source.QueryExpression is SelectExpression - { - Tables: [(PgUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }) and var sourceTable], - Predicate: var translatedPredicate, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null, - Orderings: [] - } + if ((source.TryExtractArray(out var array, ignorePredicate: true) + || source.TryConvertValuesToArray(out array, ignorePredicate: true)) + && source.QueryExpression is SelectExpression { Tables: [{ Alias: var tableAlias }], Predicate: var translatedPredicate } && translatedPredicate is null ^ predicate is null) { if (translatedPredicate is null) @@ -856,7 +797,7 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s // Contains/ContainedBy can happen for non-LTree types too, so check that Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree } - when ltreeColumn.Table == sourceTable: + when ltreeColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, @@ -864,7 +805,7 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s operatorType == PgExpressionType.Contains ? PgExpressionType.LTreeFirstAncestor : PgExpressionType.LTreeFirstDescendent, - _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), + _sqlExpressionFactory.ApplyDefaultTypeMapping(array), ltree, typeof(LTree), _typeMappingSource.FindMapping(typeof(LTree)))); @@ -880,13 +821,13 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s Left: ColumnExpression ltreeColumn, Right: var lquery } - when ltreeColumn.Table == sourceTable: + when ltreeColumn.TableAlias == tableAlias: { return BuildSimplifiedShapedQuery( source, new PgBinaryExpression( PgExpressionType.LTreeFirstMatches, - _sqlExpressionFactory.ApplyDefaultTypeMapping(GetArray(sourceTable)), + _sqlExpressionFactory.ApplyDefaultTypeMapping(array), lquery, typeof(LTree), _typeMappingSource.FindMapping(typeof(LTree)))); @@ -907,39 +848,27 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s { // Translate Skip over array to the PostgreSQL slice operator (array.Skip(2) -> array[3,]) // Note that we have unnest over multiranges, not just arrays - but multiranges don't support subscripting/slicing. - if (source.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array } unnestExpression], - GroupBy: [], - Having: null, - IsDistinct: false, - Orderings: [], - Limit: null, - Offset: null - } - && IsPostgresArray(array) - && TryGetProjectedColumn(source, out var projectedColumn) + if (source.TryExtractArray(out var array, out var projectedColumn) && TranslateExpression(count) is { } translatedCount) { -#pragma warning disable EF1001 // Internal EF Core API usage. +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; var selectExpression = new SelectExpression( - new PgUnnestExpression( - unnestExpression.Alias, - _sqlExpressionFactory.ArraySlice( - array, - lowerBound: GenerateOneBasedIndexExpression(translatedCount), - upperBound: null, - projectedColumn.IsNullable), - "value"), - "value", - projectedColumn.Type, - projectedColumn.TypeMapping, - isColumnNullable: projectedColumn.IsNullable, - // isColumnNullable: /*projectedColumn.IsNullable*/ true, // TODO: This fails because of a shaper check - identifierColumnName: "ordinality", - identifierColumnType: typeof(int), - identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))); -#pragma warning restore EF1001 // Internal EF Core API usage. + [ + new PgUnnestExpression( + tableAlias, + _sqlExpressionFactory.ArraySlice( + array, + lowerBound: GenerateOneBasedIndexExpression(translatedCount), + upperBound: null, + // isColumnNullable: /*projectedColumn.IsNullable*/ true, // TODO: This fails because of a shaper check + projectedColumn.IsNullable), + "value"), + ], + new ColumnExpression("value", tableAlias, projectedColumn.Type, projectedColumn.TypeMapping, projectedColumn.IsNullable), + identifier: [GenerateOrdinalityIdentifier(tableAlias)], + _queryCompilationContext.SqlAliasManager); +#pragma warning restore EF1001 // TODO: Simplify by using UpdateQueryExpression after https://github.com/dotnet/efcore/issues/31511 Expression shaperExpression = new ProjectionBindingExpression( @@ -970,25 +899,9 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s { // Translate Take over array to the PostgreSQL slice operator (array.Take(2) -> array[,2]) // Note that we have unnest over multiranges, not just arrays - but multiranges don't support subscripting/slicing. - if (source.QueryExpression is SelectExpression - { - Tables: [PgUnnestExpression { Array: var array } unnestExpression], - GroupBy: [], - Having: null, - IsDistinct: false, - Orderings: [], - Limit: null, - Offset: null - } - && IsPostgresArray(array) - && TryGetProjectedColumn(source, out var projectedColumn)) + if (source.TryExtractArray(out var array, out var projectedColumn) + && TranslateExpression(count) is { } translatedCount) { - var translatedCount = TranslateExpression(count); - if (translatedCount == null) - { - return base.TranslateTake(source, count); - } - PgArraySliceExpression sliceExpression; // If Skip has been called before, an array slice expression is already there; try to integrate this Take into it. @@ -1027,16 +940,13 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s projectedColumn.IsNullable); } -#pragma warning disable EF1001 // Internal EF Core API usage. +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; var selectExpression = new SelectExpression( - new PgUnnestExpression(unnestExpression.Alias, sliceExpression, "value"), - "value", - projectedColumn.Type, - projectedColumn.TypeMapping, - isColumnNullable: projectedColumn.IsNullable, - identifierColumnName: "ordinality", - identifierColumnType: typeof(int), - identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))); + [new PgUnnestExpression(tableAlias, sliceExpression, "value")], + new ColumnExpression("value", tableAlias, projectedColumn.Type, projectedColumn.TypeMapping, projectedColumn.IsNullable), + [GenerateOrdinalityIdentifier(tableAlias)], + _queryCompilationContext.SqlAliasManager); #pragma warning restore EF1001 // Internal EF Core API usage. // TODO: Simplify by using UpdateQueryExpression after https://github.com/dotnet/efcore/issues/31511 @@ -1058,6 +968,74 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s return base.TranslateTake(source, count); } + /// + /// 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 override ShapedQueryExpression? TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate) + { + // Simplify x.Array.Where(i => i != 3) => array_remove(x.Array, 3) instead of subquery + if (predicate.Body is BinaryExpression + { + NodeType: ExpressionType.NotEqual, + Left: var left, + Right: var right + } + && (left == predicate.Parameters[0] ? right : right == predicate.Parameters[0] ? left : null) is Expression itemToFilterOut + && source.TryExtractArray(out var array, out var projectedColumn) + && TranslateExpression(itemToFilterOut) is SqlExpression translatedItemToFilterOut) + { + var simplifiedTranslation = _sqlExpressionFactory.Function( + "array_remove", + [array, translatedItemToFilterOut], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + array.Type, + array.TypeMapping); + +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; + var selectExpression = new SelectExpression( + [new PgUnnestExpression(tableAlias, simplifiedTranslation, "value")], + new ColumnExpression("value", tableAlias, projectedColumn.Type, projectedColumn.TypeMapping, projectedColumn.IsNullable), + [GenerateOrdinalityIdentifier(tableAlias)], + _queryCompilationContext.SqlAliasManager); +#pragma warning restore EF1001 // Internal EF Core API usage. + + // TODO: Simplify by using UpdateQueryExpression after https://github.com/dotnet/efcore/issues/31511 + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), source.ShaperExpression.Type.MakeNullable()); + + if (source.ShaperExpression.Type != shaperExpression.Type) + { + Check.DebugAssert( + source.ShaperExpression.Type.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, source.ShaperExpression.Type); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + return base.TranslateWhere(source, predicate); + } + + /// + /// 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 override bool IsNaturallyOrdered(SelectExpression selectExpression) + => selectExpression is { Tables: [PgUnnestExpression unnest, ..] } + && (selectExpression.Orderings is [] + || selectExpression.Orderings is + [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }] + && orderingTableAlias == unnest.Alias); + /// /// 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 @@ -1145,11 +1123,7 @@ protected override bool IsValidSelectExpressionForExecuteDelete( var entityProjectionExpression = (StructuralTypeProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); var column = entityProjectionExpression.BindProperty(shaper.StructuralType.GetProperties().First()); - table = column.Table; - if (table is JoinExpressionBase joinExpressionBase) - { - table = joinExpressionBase.Table; - } + table = selectExpression.Tables.Select(t => t.UnwrapJoin()).Single(t => t.Alias == column.TableAlias); } if (table is TableExpression te) @@ -1171,40 +1145,11 @@ protected override bool IsOrdered(SelectExpression selectExpression) || selectExpression.Tables is [PgTableValuedFunctionExpression { Name: "unnest" or "jsonb_to_recordset" or "json_to_recordset" }]; - /// - /// Checks whether the given expression maps to a PostgreSQL array, as opposed to a multirange type. - /// - private static bool IsPostgresArray(SqlExpression expression) - => expression switch - { - { TypeMapping: NpgsqlArrayTypeMapping } => true, - { TypeMapping: NpgsqlMultirangeTypeMapping } => false, - { Type: var type } when type.IsMultirange() => false, - _ => true - }; - - private bool TryGetProjectedColumn( - ShapedQueryExpression shapedQueryExpression, - [NotNullWhen(true)] out ColumnExpression? projectedColumn) + private (ColumnExpression, ValueComparer) GenerateOrdinalityIdentifier(string tableAlias) { - var shaperExpression = shapedQueryExpression.ShaperExpression; - if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression - && unaryExpression.Operand.Type.IsNullableType() - && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) - { - shaperExpression = unaryExpression.Operand; - } - - if (shaperExpression is ProjectionBindingExpression projectionBindingExpression - && shapedQueryExpression.QueryExpression is SelectExpression selectExpression - && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression c) - { - projectedColumn = c; - return true; - } - - projectedColumn = null; - return false; + _ordinalityTypeMapping ??= _typeMappingSource.FindMapping("int")!; + return (new ColumnExpression("ordinality", tableAlias, typeof(int), _ordinalityTypeMapping, nullable: false), + _ordinalityTypeMapping.Comparer); } /// @@ -1216,58 +1161,18 @@ private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) ? _sqlExpressionFactory.Constant(Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) : _sqlExpressionFactory.Add(expression, _sqlExpressionFactory.Constant(1)); +#pragma warning disable EF1001 // SelectExpression constructors are currently internal private ShapedQueryExpression BuildSimplifiedShapedQuery(ShapedQueryExpression source, SqlExpression translation) => source.Update( - _sqlExpressionFactory.Select(translation), + new SelectExpression(translation, _queryCompilationContext.SqlAliasManager), Expression.Convert( new ProjectionBindingExpression(translation, new ProjectionMember(), typeof(bool?)), typeof(bool))); +#pragma warning restore EF1001 - /// - /// Extracts the out of . - /// If a is given, converts its literal values into a . - /// - private SqlExpression GetArray(TableExpressionBase tableExpression) - { - Check.DebugAssert( - tableExpression is PgUnnestExpression or ValuesExpression { ColumnNames: ["_ord", "Value"] }, - "Bad tableExpression"); - - switch (tableExpression) - { - case PgUnnestExpression unnest: - return unnest.Array; - - case ValuesExpression valuesExpression: - { - // The source table was a constant collection, so translated by default to ValuesExpression. Convert it to an unnest over - // an array constructor. - var elements = new SqlExpression[valuesExpression.RowValues.Count]; - - for (var i = 0; i < elements.Length; i++) - { - // Skip the first column (_ord) and copy the second (Value) - elements[i] = valuesExpression.RowValues[i].Values[1]; - } - - return new PgNewArrayExpression( - elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null); - } - - default: - throw new ArgumentException(nameof(tableExpression)); - } - } - - private sealed class OuterReferenceFindingExpressionVisitor : ExpressionVisitor + private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor { - private readonly TableExpression _mainTable; private bool _containsReference; - public OuterReferenceFindingExpressionVisitor(TableExpression mainTable) - { - _mainTable = mainTable; - } - public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) { _containsReference = false; @@ -1285,8 +1190,7 @@ public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) return expression; } - if (expression is ColumnExpression columnExpression - && columnExpression.Table == _mainTable) + if (expression is ColumnExpression { TableAlias: var tableAlias} && tableAlias == mainTable.Alias) { _containsReference = true; @@ -1296,62 +1200,4 @@ public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) return base.Visit(expression); } } - - /// - /// 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 class NpgsqlInferredTypeMappingApplier : RelationalInferredTypeMappingApplier - { - private readonly NpgsqlTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - /// - /// 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 NpgsqlInferredTypeMappingApplier( - IModel model, - NpgsqlTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping?> inferredTypeMappings) - : base(model, sqlExpressionFactory, inferredTypeMappings) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - /// 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 override Expression VisitExtension(Expression expression) - { - switch (expression) - { - case PgUnnestExpression unnestExpression - when TryGetInferredTypeMapping(unnestExpression, unnestExpression.ColumnName, out var elementTypeMapping): - { - var collectionTypeMapping = _typeMappingSource.FindMapping(unnestExpression.Array.Type, Model, elementTypeMapping); - - if (collectionTypeMapping is null) - { - throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(expression.Print())); - } - - return unnestExpression.Update( - _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping)); - } - - default: - return base.VisitExtension(expression); - } - } - } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs index ad06c1e83..39d8377f7 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -1,3 +1,5 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// @@ -8,6 +10,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// public class NpgsqlQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory { + private readonly INpgsqlSingletonOptions _npgsqlSingletonOptions; + /// /// 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 @@ -16,10 +20,12 @@ public class NpgsqlQueryableMethodTranslatingExpressionVisitorFactory : IQueryab /// public NpgsqlQueryableMethodTranslatingExpressionVisitorFactory( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, - RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies) + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + INpgsqlSingletonOptions npgsqlSingletonOptions) { Dependencies = dependencies; RelationalDependencies = relationalDependencies; + _npgsqlSingletonOptions = npgsqlSingletonOptions; } /// @@ -45,5 +51,9 @@ public NpgsqlQueryableMethodTranslatingExpressionVisitorFactory( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) - => new NpgsqlQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext); + => new NpgsqlQueryableMethodTranslatingExpressionVisitor( + Dependencies, + RelationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext, + _npgsqlSingletonOptions); } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypeResolutionCompensatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypeResolutionCompensatingExpressionVisitor.cs deleted file mode 100644 index 1b7e53079..000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypeResolutionCompensatingExpressionVisitor.cs +++ /dev/null @@ -1,127 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlSetOperationTypeResolutionCompensatingExpressionVisitor : ExpressionVisitor -{ - private State _state; - - /// - /// 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 override Expression VisitExtension(Expression extensionExpression) - => extensionExpression switch - { - ShapedQueryExpression shapedQueryExpression - => shapedQueryExpression.Update( - Visit(shapedQueryExpression.QueryExpression), - Visit(shapedQueryExpression.ShaperExpression)), - SetOperationBase setOperationExpression => VisitSetOperation(setOperationExpression), - SelectExpression selectExpression => VisitSelect(selectExpression), - _ => base.VisitExtension(extensionExpression) - }; - - private Expression VisitSetOperation(SetOperationBase setOperationExpression) - { - switch (_state) - { - case State.Nothing: - _state = State.InSingleSetOperation; - var visited = base.VisitExtension(setOperationExpression); - _state = State.Nothing; - return visited; - - case State.InSingleSetOperation: - _state = State.InNestedSetOperation; - visited = base.VisitExtension(setOperationExpression); - _state = State.InSingleSetOperation; - return visited; - - default: - return base.VisitExtension(setOperationExpression); - } - } - - private Expression VisitSelect(SelectExpression selectExpression) - { - var changed = false; - - var tables = new List(); - foreach (var table in selectExpression.Tables) - { - var newTable = (TableExpressionBase)Visit(table); - changed |= newTable != table; - tables.Add(newTable); - } - - // Above we visited the tables, which may contain nested set operations - so we retained our state. - // When visiting the below elements, reset to state to properly handle nested unions inside e.g. the predicate. - var parentState = _state; - _state = State.Nothing; - - var projections = new List(); - foreach (var item in selectExpression.Projection) - { - // Inject an explicit cast node around null literals - var updatedProjection = parentState == State.InNestedSetOperation && item.Expression is SqlConstantExpression { Value : null } - ? item.Update( - new SqlUnaryExpression(ExpressionType.Convert, item.Expression, item.Expression.Type, item.Expression.TypeMapping)) - : (ProjectionExpression)Visit(item); - - projections.Add(updatedProjection); - changed |= updatedProjection != item; - } - - var predicate = (SqlExpression?)Visit(selectExpression.Predicate); - changed |= predicate != selectExpression.Predicate; - - var groupBy = new List(); - foreach (var groupingKey in selectExpression.GroupBy) - { - var newGroupingKey = (SqlExpression)Visit(groupingKey); - changed |= newGroupingKey != groupingKey; - groupBy.Add(newGroupingKey); - } - - var havingExpression = (SqlExpression?)Visit(selectExpression.Having); - changed |= havingExpression != selectExpression.Having; - - var orderings = new List(); - foreach (var ordering in selectExpression.Orderings) - { - var orderingExpression = (SqlExpression)Visit(ordering.Expression); - changed |= orderingExpression != ordering.Expression; - orderings.Add(ordering.Update(orderingExpression)); - } - - var offset = (SqlExpression?)Visit(selectExpression.Offset); - changed |= offset != selectExpression.Offset; - - var limit = (SqlExpression?)Visit(selectExpression.Limit); - changed |= limit != selectExpression.Limit; - - // If we were in the InNestedSetOperation state, we've applied all explicit type mappings when visiting the ProjectionExpressions - // above; change the state to prevent unnecessarily continuing to compensate - _state = parentState == State.InNestedSetOperation ? State.AlreadyCompensated : parentState; - - return changed - ? selectExpression.Update( - projections, tables, predicate, groupBy, havingExpression, orderings, limit, offset) - : selectExpression; - } - - private enum State - { - Nothing, - InSingleSetOperation, - InNestedSetOperation, - AlreadyCompensated - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs b/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs new file mode 100644 index 000000000..6086bf436 --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs @@ -0,0 +1,80 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +/// +/// A visitor that injects explicit typing on null projections in set operations, to ensure PostgreSQL gets the typing right. +/// +/// +/// +/// See the +/// PostgreSQL docs on type conversion and set operations. +/// +/// +/// 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 NpgsqlSetOperationTypingInjector : ExpressionVisitor +{ + /// + /// 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 override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch + { + ShapedQueryExpression shapedQueryExpression + => shapedQueryExpression.Update( + Visit(shapedQueryExpression.QueryExpression), + Visit(shapedQueryExpression.ShaperExpression)), + + SetOperationBase setOperationExpression => VisitSetOperation(setOperationExpression), + + _ => base.VisitExtension(extensionExpression) + }; + + private Expression VisitSetOperation(SetOperationBase setOperation) + { + var select1 = (SelectExpression)Visit(setOperation.Source1); + var select2 = (SelectExpression)Visit(setOperation.Source2); + + List? rewrittenProjections = null; + + for (var i = 0; i < select1.Projection.Count; i++) + { + var projection = select1.Projection[i]; + var visitedProjection = projection.Expression is SqlConstantExpression { Value : null } + && select2.Projection[i].Expression is SqlConstantExpression { Value : null } + ? projection.Update( + new SqlUnaryExpression( + ExpressionType.Convert, projection.Expression, projection.Expression.Type, projection.Expression.TypeMapping)) + : (ProjectionExpression)Visit(projection); + + if (visitedProjection != projection && rewrittenProjections is null) + { + rewrittenProjections = new List(select1.Projection.Count); + rewrittenProjections.AddRange(select1.Projection.Take(i)); + } + + rewrittenProjections?.Add(visitedProjection); + } + + if (rewrittenProjections is not null) + { + select1 = select1.Update( + select1.Tables, + select1.Predicate, + select1.GroupBy, + select1.Having, + rewrittenProjections, + select1.Orderings, + select1.Offset, + select1.Limit); + } + + return setOperation.Update(select1, select2); + } +} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs index fdb885b22..6424f37e5 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs @@ -11,14 +11,15 @@ public class NpgsqlSqlNullabilityProcessor : SqlNullabilityProcessor private readonly ISqlExpressionFactory _sqlExpressionFactory; /// - /// Creates a new instance of the class. + /// 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. /// - /// Parameter object containing dependencies for this class. - /// A bool value indicating whether relational null semantics are in use. public NpgsqlSqlNullabilityProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { _sqlExpressionFactory = dependencies.SqlExpressionFactory; } @@ -29,144 +30,170 @@ protected override SqlExpression VisitSqlBinary( bool allowOptimizedExpansion, out bool nullable) { - if (sqlBinaryExpression is not + return sqlBinaryExpression switch + { { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, Left: PgRowValueExpression leftRowValue, Right: PgRowValueExpression rightRowValue - }) + } + => VisitRowValueComparison(sqlBinaryExpression.OperatorType, leftRowValue, rightRowValue, out nullable), + + _ => base.VisitSqlBinary(sqlBinaryExpression, allowOptimizedExpansion, out nullable) + }; + + SqlExpression VisitRowValueComparison( + ExpressionType operatorType, + PgRowValueExpression leftRowValue, + PgRowValueExpression rightRowValue, + out bool nullable) { - return base.VisitSqlBinary(sqlBinaryExpression, allowOptimizedExpansion, out nullable); - } + // Equality checks between row values require some special null semantics compensation. + // Row value equality/inequality works the same as regular equals/non-equals; this means that it's fine as long as we're + // comparing non-nullable values (no need to compensate), but for nullable values, we need to compensate. We go over the value + // pairs, and extract out pairs that require compensation to an expanded, non-value-tuple expression (regular equality null + // semantics). Note that PostgreSQL does have DISTINCT FROM/NOT DISTINCT FROM which would have been perfect here, but those + // still don't use indexes. + // Note that we don't do compensation for comparisons (e.g. greater than) since these are expressed via EF.Functions, which + // correspond directly to SQL constructs. + // The PG docs around this are in https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON + + Check.DebugAssert(leftRowValue.Values.Count == rightRowValue.Values.Count, "left.Values.Count == right.Values.Count"); + var count = leftRowValue.Values.Count; + + SqlExpression? expandedExpression = null; + List? visitedLeftValues = null; + List? visitedRightValues = null; - // Equality checks between row values require some special null semantics compensation. - // Row value equality/inequality works the same as regular equals/non-equals; this means that it's fine as long as we're comparing - // non-nullable values (no need to compensate), but for nullable values, we need to compensate. We go over the value pairs, and - // extract out pairs that require compensation to an expanded, non-value-tuple expression (regular equality null semantics). - // Note that PostgreSQL does have DISTINCT FROM/NOT DISTINCT FROM which would have been perfect here, but those still don't use - // indexes. - // Note that we don't do compensation for comparisons (e.g. greater than) since these are expressed via EF.Functions, which - // correspond directly to SQL constructs. - // The PG docs around this are in https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON + for (var i = 0; i < count; i++) + { + // Visit the left value, populating visitedLeftValues only if we haven't yet switched to an expanded expression, and only if + // the visitation actually changed something + var leftValue = leftRowValue.Values[i]; + var rightValue = rightRowValue.Values[i]; + var visitedLeftValue = Visit(leftRowValue.Values[i], out var leftNullable); + var visitedRightValue = Visit(rightRowValue.Values[i], out var rightNullable); + + // If both sides are non-nullable, no null expansion is required; the same is true if we're doing equality in optimized + // mode, and only one side is nullable (WHERE (NonNullable1, NonNullable2) = (NonNullable3, Nullable4)). + if (!leftNullable && !rightNullable + || allowOptimizedExpansion && operatorType is ExpressionType.Equal && (!leftNullable || !rightNullable)) + { + // The comparison for this value pair doesn't require expansion and can remain in the row value (so continue below). + // But if the visitation above changed a value, construct a list to hold the visited values. + if (visitedLeftValue != leftValue && visitedLeftValues is null) + { + visitedLeftValues = SliceToList(leftRowValue.Values, count, i); + } - Check.DebugAssert(leftRowValue.Values.Count == rightRowValue.Values.Count, "left.Values.Count == right.Values.Count"); - var count = leftRowValue.Values.Count; + visitedLeftValues?.Add(visitedLeftValue); - var operatorType = sqlBinaryExpression.OperatorType; + if (visitedRightValue != rightValue && visitedRightValues is null) + { + visitedRightValues = SliceToList(rightRowValue.Values, count, i); + } - SqlExpression? expandedExpression = null; - List? visitedLeftValues = null; - List? visitedRightValues = null; + visitedRightValues?.Add(visitedRightValue); - for (var i = 0; i < count; i++) - { - // Visit the left value, populating visitedLeftValues only if we haven't yet switched to an expanded expression, and only if - // the visitation actually changed something, and - var leftValue = leftRowValue.Values[i]; - var rightValue = rightRowValue.Values[i]; - var visitedRightValue = Visit(rightRowValue.Values[i], out var rightNullable); - var visitedLeftValue = Visit(leftRowValue.Values[i], out var leftNullable); - - if (!leftNullable && !rightNullable - || allowOptimizedExpansion && (!leftNullable || !rightNullable)) - { - // The comparison for this value pair doesn't require expansion and can remain in the row value (so continue below). - // But if the visitation above changed a value, construct a list to hold the visited values. - if (visitedLeftValue != leftValue && visitedLeftValues is null) - { - visitedLeftValues = SliceToList(leftRowValue.Values, count, i); + continue; } - if (visitedLeftValues is not null) - { - visitedLeftValues.Add(visitedLeftValue); - } + // If we're here, the value pair requires null semantics compensation. We build a binary expression around the pair and + // visit that (that adds the compensation). We then chain all such expressions together with AND. + var valueBinaryExpression = Visit( + _sqlExpressionFactory.MakeBinary( + operatorType, visitedLeftValue, visitedRightValue, typeMapping: null, existingExpression: sqlBinaryExpression)!, + allowOptimizedExpansion, + out _); - if (visitedRightValue != rightValue && visitedRightValues is null) + if (expandedExpression is null) { + // visitedLeft/RightValues will contain all pairs that can remain in the row value (since they don't require + // compensation) + visitedLeftValues = SliceToList(leftRowValue.Values, count, i); visitedRightValues = SliceToList(rightRowValue.Values, count, i); - } - if (visitedRightValues is not null) + expandedExpression = valueBinaryExpression; + } + else { - visitedRightValues.Add(visitedRightValue); + expandedExpression = operatorType switch + { + ExpressionType.Equal => _sqlExpressionFactory.AndAlso(expandedExpression, valueBinaryExpression), + ExpressionType.NotEqual => _sqlExpressionFactory.OrElse(expandedExpression, valueBinaryExpression), + _ => throw new UnreachableException() + }; } - - continue; } - // If we're here, the value pair requires null semantics compensation. We build a binary expression around the pair and visit - // that (that adds the compensation). We then chain all such expressions together with AND. - var valueBinaryExpression = Visit( - _sqlExpressionFactory.MakeBinary(operatorType, visitedLeftValue, visitedRightValue, null)!, allowOptimizedExpansion, out _); + // TODO: This is wrong, need to properly calculate this: #3250 + nullable = false; if (expandedExpression is null) { - // visitedLeft/RightValues will contain all pairs that can remain in the row value (since they don't require compensation) - visitedLeftValues = SliceToList(leftRowValue.Values, count, i); - visitedRightValues = SliceToList(rightRowValue.Values, count, i); - - expandedExpression = valueBinaryExpression; + // No pairs required compensation, so they all stay in the row value. + // Either return the original binary expression (if no value visitation changed anything), or construct a new one over the + // visited values. + return visitedLeftValues is null && visitedRightValues is null + ? sqlBinaryExpression + : _sqlExpressionFactory.MakeBinary( + operatorType, + visitedLeftValues is null + ? leftRowValue + : new PgRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), + visitedRightValues is null + ? rightRowValue + : new PgRowValueExpression(visitedRightValues, leftRowValue.Type, leftRowValue.TypeMapping), + typeMapping: null, + existingExpression: sqlBinaryExpression)!; } - else + + Check.DebugAssert(visitedLeftValues is not null, "visitedLeftValues is not null"); + Check.DebugAssert(visitedRightValues is not null, "visitedRightValues is not null"); + + // All pairs required compensation - none are left in the row value comparison. Just return the expanded expression. + if (visitedLeftValues.Count is 0) { - expandedExpression = _sqlExpressionFactory.AndAlso(expandedExpression, valueBinaryExpression); + return expandedExpression; } - } - - nullable = false; - if (expandedExpression is null) - { - // No pairs required compensation, so they all stay in the row value. - // Either return the original binary expression (if no value visitation changed anything), or construct a new one over the - // visited values. - return visitedLeftValues is null && visitedRightValues is null - ? sqlBinaryExpression + // Some pairs required compensation; we're going to return a combined expression of the unexpanded pairs (which didn't require + // compensation) and of the expanded expression. + // If there's only one unexpanded pair left, that's a special case that doesn't require row values (just a regular pair + // comparison). Otherwise create the new row comparison expression for the unexpanded pairs. + var unexpandedExpression = visitedLeftValues.Count is 1 + ? _sqlExpressionFactory.MakeBinary(operatorType, visitedLeftValues[0], visitedRightValues[0], typeMapping: null)! : _sqlExpressionFactory.MakeBinary( operatorType, - visitedLeftValues is null - ? leftRowValue - : new PgRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), - visitedRightValues is null - ? rightRowValue - : new PgRowValueExpression(visitedRightValues, leftRowValue.Type, leftRowValue.TypeMapping), - null)!; - } - - Check.DebugAssert(visitedLeftValues is not null, "visitedLeftValues is not null"); - Check.DebugAssert(visitedRightValues is not null, "visitedRightValues is not null"); + new PgRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), + new PgRowValueExpression(visitedRightValues, rightRowValue.Type, rightRowValue.TypeMapping), + typeMapping: null)!; - // Some pairs required compensation. Combine the pairs which didn't (in visitedLeft/RightValues) with expandedExpression - // (which contains the logic for those that did). - return visitedLeftValues.Count switch - { - 0 => expandedExpression, - 1 => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.MakeBinary(operatorType, visitedLeftValues[0], visitedRightValues[0], null)!, - expandedExpression), // Technically the CLR type and type mappings are incorrect, as we're truncating the row values. // But that shouldn't matter. - _ => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.MakeBinary( - sqlBinaryExpression.OperatorType, - new PgRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), - new PgRowValueExpression(visitedRightValues, rightRowValue.Type, rightRowValue.TypeMapping), - null)!, - expandedExpression) - }; + return _sqlExpressionFactory.MakeBinary( + operatorType: operatorType switch + { + ExpressionType.Equal => ExpressionType.AndAlso, + ExpressionType.NotEqual => ExpressionType.OrElse, + _ => throw new UnreachableException() + }, + unexpandedExpression, + expandedExpression, + typeMapping: null)!; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static List SliceToList(IReadOnlyList source, int capacity, int count) + { + var list = new List(capacity); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static List SliceToList(IReadOnlyList source, int capacity, int count) - { - var list = new List(capacity); + for (var i = 0; i < count; i++) + { + list.Add(source[i]); + } - for (var i = 0; i < count; i++) - { - list.Add(source[i]); + return list; } - - return list; } } @@ -261,7 +288,7 @@ protected virtual SqlExpression VisitAny(PgAnyExpression anyExpression, bool all _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function( "array_position", - new[] { array, _sqlExpressionFactory.Constant(null, item.TypeMapping) }, + [array, _sqlExpressionFactory.Constant(null, item.Type, item.TypeMapping)], nullable: true, argumentsPropagateNullability: FalseArrays[2], typeof(int))))); @@ -457,21 +484,70 @@ PgFunctionExpression VisitPostgresFunctionComponents(PgFunctionExpression pgFunc /// protected virtual SqlExpression VisitILike(PgILikeExpression iLikeExpression, bool allowOptimizedExpansion, out bool nullable) { - Check.NotNull(iLikeExpression, nameof(iLikeExpression)); + // Note: this is largely duplicated from relational SqlNullabilityProcessor.VisitLike. + // We unfortunately can't reuse that since it may return arbitrary expression tree structures with LikeExpression embedded, but + // we need ILikeExpression (see #3034). + var match = Visit(iLikeExpression.Match, out var matchNullable); + var pattern = Visit(iLikeExpression.Pattern, out var patternNullable); + var escapeChar = Visit(iLikeExpression.EscapeChar, out var escapeCharNullable); + + SqlExpression result = iLikeExpression.Update(match, pattern, escapeChar); + + if (UseRelationalNulls) + { + nullable = matchNullable || patternNullable || escapeCharNullable; - var like = new LikeExpression( - iLikeExpression.Match, - iLikeExpression.Pattern, - iLikeExpression.EscapeChar, - iLikeExpression.TypeMapping); + return result; + } - var visited = base.VisitLike(like, allowOptimizedExpansion, out nullable); + nullable = false; - return visited == like - ? iLikeExpression - : visited is LikeExpression visitedLike - ? iLikeExpression.Update(visitedLike.Match, visitedLike.Pattern, visitedLike.EscapeChar) - : visited; + // The null semantics behavior we implement for LIKE is that it only returns true when both sides are non-null and match; any other + // input returns false: + // foo LIKE f% -> true + // foo LIKE null -> false + // null LIKE f% -> false + // null LIKE null -> false + + if (IsNull(match) || IsNull(pattern) || IsNull(escapeChar)) + { + return _sqlExpressionFactory.Constant(false, iLikeExpression.TypeMapping); + } + + // A constant match-all pattern (%) returns true for all cases, except where the match string is null: + // nullable_foo LIKE % -> foo IS NOT NULL + // non_nullable_foo LIKE % -> true + if (pattern is SqlConstantExpression { Value: "%" }) + { + return matchNullable + ? _sqlExpressionFactory.IsNotNull(match) + : _sqlExpressionFactory.Constant(true, iLikeExpression.TypeMapping); + } + + if (!allowOptimizedExpansion) + { + if (matchNullable) + { + result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(match)); + } + + if (patternNullable) + { + result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(pattern)); + } + + if (escapeChar is not null && escapeCharNullable) + { + result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(escapeChar)); + } + + // TODO: This revisits the operand; ideally we'd call ProcessNullNotNull directly but that's private + SqlExpression GenerateNotNullCheck(SqlExpression operand) + => _sqlExpressionFactory.Not( + Visit(_sqlExpressionFactory.IsNull(operand), allowOptimizedExpansion, out _)); + } + + return result; } /// @@ -657,4 +733,9 @@ private static bool MayContainNulls(SqlExpression arrayExpression) return true; } + + // Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool. + private bool IsNull(SqlExpression? expression) + => expression is SqlConstantExpression { Value: null } + || expression is SqlParameterExpression { Name: string parameterName } && ParameterValues[parameterName] is null; } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index 672095dab..7e2fd3d47 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; using System.Runtime.CompilerServices; using System.Text; @@ -29,26 +30,26 @@ public class NpgsqlSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExp private static Type? _nodaTimePeriodType; private static readonly ConstructorInfo DateTimeCtor1 = - typeof(DateTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int)])!; private static readonly ConstructorInfo DateTimeCtor2 = - typeof(DateTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int) })!; + typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; private static readonly ConstructorInfo DateTimeCtor3 = typeof(DateTime).GetConstructor( - new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(DateTimeKind) })!; + [typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(DateTimeKind)])!; private static readonly ConstructorInfo DateOnlyCtor = - typeof(DateOnly).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; + typeof(DateOnly).GetConstructor([typeof(int), typeof(int), typeof(int)])!; private static readonly MethodInfo StringStartsWithMethod - = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!; + = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), [typeof(string)])!; private static readonly MethodInfo StringEndsWithMethod - = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!; + = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), [typeof(string)])!; private static readonly MethodInfo StringContainsMethod - = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) })!; + = typeof(string).GetRuntimeMethod(nameof(string.Contains), [typeof(string)])!; private static readonly MethodInfo EscapeLikePatternParameterMethod = typeof(NpgsqlSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!; @@ -99,7 +100,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) { return _sqlExpressionFactory.Function( "length", - new[] { sqlOperand }, + [sqlOperand], nullable: true, argumentsPropagateNullability: TrueArrays[1], typeof(int)); @@ -194,8 +195,8 @@ when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.Local return PgFunctionExpression.CreateWithNamedArguments( "make_interval", - new[] { subtraction }, - new[] { "days" }, + [subtraction], + ["days"], nullable: true, argumentsPropagateNullability: TrueArrays[1], builtIn: true, @@ -217,21 +218,39 @@ when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.Local var translation = base.VisitBinary(binaryExpression); - // A somewhat hacky workaround for #2942. - // When an optional owned JSON entity is compared to null, we get WHERE (x -> y) IS NULL. - // The -> operator (returning jsonb) is used rather than ->> (returning text), since an entity type is being extracted, and further - // JSON operations may need to be composed. However, when the value extracted is a JSON null, a non-NULL jsonb value is returned, - // and comparing that to relational NULL returns false. - // Pattern-match this and force the use of ->> by changing the mapping to be a scalar rather than an entity type. - if (translation is SqlUnaryExpression + switch (translation) + { + // Optimize (x - c) - (y - c) to x - y. + // This is particularly useful for DateOnly.DayNumber - DateOnly.DayNumber, which is the way to express DateOnly subtraction + // (the subtraction operator isn't defined over DateOnly in .NET). The translation of x.DayNumber is x - DATE '0001-01-01', + // so the below is a useful simplification. + // TODO: As this is a generic mathematical simplification, we should move it to a generic optimization phase in EF Core. + case SqlBinaryExpression + { + OperatorType: ExpressionType.Subtract, + Left: SqlBinaryExpression { OperatorType: ExpressionType.Subtract, Left: var left1, Right: var right1 }, + Right: SqlBinaryExpression { OperatorType: ExpressionType.Subtract, Left: var left2, Right: var right2 } + } originalBinary when right1.Equals(right2): + { + return new SqlBinaryExpression(ExpressionType.Subtract, left1, left2, originalBinary.Type, originalBinary.TypeMapping); + } + + // A somewhat hacky workaround for #2942. + // When an optional owned JSON entity is compared to null, we get WHERE (x -> y) IS NULL. + // The -> operator (returning jsonb) is used rather than ->> (returning text), since an entity type is being extracted, and + // further JSON operations may need to be composed. However, when the value extracted is a JSON null, a non-NULL jsonb value is + // returned, and comparing that to relational NULL returns false. + // Pattern-match this and force the use of ->> by changing the mapping to be a scalar rather than an entity type. + case SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, Operand: JsonScalarExpression { TypeMapping: NpgsqlOwnedJsonTypeMapping } operand - } unary) - { - return unary.Update( - new JsonScalarExpression( - operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); + } unary: + { + return unary.Update( + new JsonScalarExpression( + operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); + } } return translation; @@ -345,11 +364,13 @@ protected override Expression VisitNew(NewExpression newExpression) rewrittenArguments.Add(_sqlExpressionFactory.Constant("UTC")); } - return kind == DateTimeKind.Utc - ? _sqlExpressionFactory.Function( - "make_timestamptz", rewrittenArguments, nullable: true, TrueArrays[8], typeof(DateTime), _timestampTzMapping) - : _sqlExpressionFactory.Function( - "make_timestamp", rewrittenArguments, nullable: true, TrueArrays[7], typeof(DateTime), _timestampMapping); + return _sqlExpressionFactory.Function( + kind == DateTimeKind.Utc ? "make_timestamptz" : "make_timestamp", + rewrittenArguments, + nullable: true, + TrueArrays[rewrittenArguments.Count], + typeof(DateTime), + kind == DateTimeKind.Utc ? _timestampTzMapping : _timestampMapping); } } @@ -410,7 +431,9 @@ private bool TryTranslateStartsEndsWithContains( // simple LIKE translation = patternConstant.Value switch { - null => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant(null, stringTypeMapping)), + null => _sqlExpressionFactory.Like( + translatedInstance, + _sqlExpressionFactory.Constant(null, typeof(string), stringTypeMapping)), // In .NET, all strings start with/end with/contain the empty string, but SQL LIKE return false for empty patterns. // Return % which always matches instead. @@ -450,12 +473,12 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref QueryCompilationContext.QueryContextParameter); var escapedPatternParameter = - _queryCompilationContext.RegisterRuntimeParameter(patternParameter.Name + "_rewritten", lambda); + _queryCompilationContext.RegisterRuntimeParameter( + $"{patternParameter.Name}_{methodType.ToString().ToLower(CultureInfo.InvariantCulture)}", lambda); translation = _sqlExpressionFactory.Like( translatedInstance, - new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping), - _sqlExpressionFactory.Constant(LikeEscapeChar.ToString())); + new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping)); return true; } @@ -472,13 +495,12 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref translation = _sqlExpressionFactory.Function( methodType is StartsEndsWithContains.StartsWith ? "left" : "right", - new[] - { + [ translatedInstance, _sqlExpressionFactory.Function( - "length", new[] { translatedPattern }, nullable: true, - argumentsPropagateNullability: new[] { true }, typeof(int)) - }, nullable: true, argumentsPropagateNullability: new[] { true, true }, typeof(string), + "length", [translatedPattern], nullable: true, + argumentsPropagateNullability: [true], typeof(int)) + ], nullable: true, argumentsPropagateNullability: [true, true], typeof(string), stringTypeMapping); // LEFT/RIGHT of a citext return a text, so for non-default text mappings we apply an explicit cast. @@ -508,8 +530,8 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref _sqlExpressionFactory.IsNotNull(translatedPattern), _sqlExpressionFactory.GreaterThan( _sqlExpressionFactory.Function( - "strpos", new[] { translatedInstance, translatedPattern }, nullable: true, - argumentsPropagateNullability: new[] { true, true }, typeof(int)), + "strpos", [translatedInstance, translatedPattern], nullable: true, + argumentsPropagateNullability: [true, true], typeof(int)), _sqlExpressionFactory.Constant(0)))); break; @@ -521,7 +543,13 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref } } - private static string? ConstructLikePatternParameter( + /// + /// 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 string? ConstructLikePatternParameter( QueryContext queryContext, string baseParameterName, StartsEndsWithContains methodType) @@ -544,10 +572,36 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref _ => throw new UnreachableException() }; - private enum StartsEndsWithContains + /// + /// 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 enum StartsEndsWithContains { + /// + /// 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. + /// StartsWith, + + /// + /// 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. + /// EndsWith, + + /// + /// 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. + /// Contains } @@ -573,6 +627,44 @@ private static string EscapeLikePattern(string pattern) #endregion StartsWith/EndsWith/Contains + #region GREATEST/LEAST + + /// + /// 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 override SqlExpression GenerateGreatest(IReadOnlyList expressions, Type resultType) + { + // Docs: https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + + // If one or more arguments aren't NULL, then NULL arguments are ignored during comparison. + // If all arguments are NULL, then GREATEST returns NULL. + return _sqlExpressionFactory.Function( + "GREATEST", expressions, nullable: true, Enumerable.Repeat(false, expressions.Count), resultType, resultTypeMapping); + } + + /// + /// 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 override SqlExpression GenerateLeast(IReadOnlyList expressions, Type resultType) + { + // Docs: https://www.postgresql.org/docs/current/functions-conditional.html#FUNCTIONS-GREATEST-LEAST + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + + // If one or more arguments aren't NULL, then NULL arguments are ignored during comparison. + // If all arguments are NULL, then LEAST returns NULL. + return _sqlExpressionFactory.Function( + "LEAST", expressions, nullable: true, Enumerable.Repeat(false, expressions.Count), resultType, resultTypeMapping); + } + + #endregion GREATEST/LEAST + #region Copied from RelationalSqlTranslatingExpressionVisitor private static Expression TryRemoveImplicitConvert(Expression expression) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs new file mode 100644 index 000000000..bed46f91f --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs @@ -0,0 +1,56 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using ColumnInfo = Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal.PgTableValuedFunctionExpression.ColumnInfo; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlSqlTreePruner : SqlTreePruner +{ + /// + /// 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 override Expression VisitExtension(Expression node) + { + switch (node) + { + case PgTableValuedFunctionExpression { ColumnInfos: IReadOnlyList columnInfos } tvf: + var arguments = this.Visit(tvf.Arguments); + + List? newColumnInfos = null; + + if (ReferencedColumnMap.TryGetValue(tvf.Alias, out var referencedAliases)) + { + for (var i = 0; i < columnInfos.Count; i++) + { + if (referencedAliases.Contains(columnInfos[i].Name)) + { + newColumnInfos?.Add(columnInfos[i]); + } + else if (newColumnInfos is null) + { + newColumnInfos = []; + for (var j = 0; j < i; j++) + { + newColumnInfos.Add(columnInfos[j]); + } + } + } + } + + return tvf + .Update(arguments) + .WithColumnInfos(newColumnInfos ?? columnInfos); + + default: + return base.VisitExtension(node); + } + } +} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs new file mode 100644 index 000000000..62a6444ec --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs @@ -0,0 +1,62 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.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 NpgsqlTypeMappingPostprocessor : RelationalTypeMappingPostprocessor +{ + private readonly IModel _model; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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 NpgsqlTypeMappingPostprocessor( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + RelationalQueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + _model = queryCompilationContext.Model; + _typeMappingSource = relationalDependencies.TypeMappingSource; + _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; + } + + /// + /// 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 override Expression VisitExtension(Expression expression) + { + switch (expression) + { + case PgUnnestExpression unnestExpression + when TryGetInferredTypeMapping(unnestExpression.Alias, unnestExpression.ColumnName, out var elementTypeMapping): + { + var collectionTypeMapping = _typeMappingSource.FindMapping(unnestExpression.Array.Type, _model, elementTypeMapping); + + if (collectionTypeMapping is null) + { + throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(expression.Print())); + } + + return unnestExpression.Update( + _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping)); + } + + default: + return base.VisitExtension(expression); + } + } +} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs index 826a7e2aa..b4a1d578c 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs @@ -27,25 +27,29 @@ public class NpgsqlUnnestPostprocessor : ExpressionVisitor switch (expression) { case ShapedQueryExpression shapedQueryExpression: - return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); + return shapedQueryExpression + .UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)) + .UpdateShaperExpression(Visit(shapedQueryExpression.ShaperExpression)); case SelectExpression selectExpression: { TableExpressionBase[]? newTables = null; + var orderings = selectExpression.Orderings; + for (var i = 0; i < selectExpression.Tables.Count; i++) { var table = selectExpression.Tables[i]; + var unwrappedTable = table.UnwrapJoin(); // Find any unnest table which does not have any references to its ordinality column in the projection or orderings - // (this is where they may appear when a column is an identifier). - var unnest = table as PgUnnestExpression ?? (table as JoinExpressionBase)?.Table as PgUnnestExpression; - if (unnest is not null - && !selectExpression.Orderings.Select(o => o.Expression) + // (this is where they may appear); if found, remove the ordinality column from the unnest call. + // Note that if the ordinality column is the first ordering, we can still remove it, since unnest already returns + // ordered results. + if (unwrappedTable is PgUnnestExpression unnest + && !selectExpression.Orderings.Skip(1).Select(o => o.Expression) .Concat(selectExpression.Projection.Select(p => p.Expression)) - .Any( - p => p is ColumnExpression { Name: "ordinality", Table: var ordinalityTable } - && ordinalityTable == table)) + .Any(IsOrdinalityColumn)) { if (newTables is null) { @@ -65,21 +69,30 @@ public class NpgsqlUnnestPostprocessor : ExpressionVisitor PgUnnestExpression => newUnnest, _ => throw new UnreachableException() }; + + if (orderings.Count > 0 && IsOrdinalityColumn(orderings[0].Expression)) + { + orderings = orderings.Skip(1).ToList(); + } } + + bool IsOrdinalityColumn(SqlExpression expression) + => expression is ColumnExpression { Name: "ordinality" } ordinalityColumn + && ordinalityColumn.TableAlias == unwrappedTable.Alias; } return base.Visit( newTables is null ? selectExpression : selectExpression.Update( - selectExpression.Projection, newTables, selectExpression.Predicate, selectExpression.GroupBy, selectExpression.Having, - selectExpression.Orderings, - selectExpression.Limit, - selectExpression.Offset)); + selectExpression.Projection, + orderings, + selectExpression.Offset, + selectExpression.Limit)); } default: diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 6abd9031d..73c64c92c 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -151,7 +151,7 @@ public virtual PgJsonTraversalExpression JsonTraversal( bool returnsText, Type type, RelationalTypeMapping? typeMapping = null) - => JsonTraversal(expression, Array.Empty(), returnsText, type, typeMapping); + => JsonTraversal(expression, [], returnsText, type, typeMapping); /// /// Creates a new , for traversing inside a JSON document. @@ -198,7 +198,7 @@ public virtual SqlExpression NewArrayOrConstant( var addMethod = type.GetMethod("Add")!; for (var i = 0; i < elements.Count; i++) { - addMethod.Invoke(list, new[] { ((SqlConstantExpression)newArrayExpression.Expressions[i]).Value }); + addMethod.Invoke(list, [((SqlConstantExpression)newArrayExpression.Expressions[i]).Value]); } return Constant(list, newArrayExpression.TypeMapping); @@ -225,11 +225,12 @@ public virtual PgNewArrayExpression NewArray( => (PgNewArrayExpression)ApplyTypeMapping(new PgNewArrayExpression(expressions, type, typeMapping), typeMapping); /// - public override SqlBinaryExpression? MakeBinary( + public override SqlExpression? MakeBinary( ExpressionType operatorType, SqlExpression left, SqlExpression right, - RelationalTypeMapping? typeMapping) + RelationalTypeMapping? typeMapping, + SqlExpression? existingExpr = null) { switch (operatorType) { @@ -268,7 +269,7 @@ public virtual PgNewArrayExpression NewArray( } } - return base.MakeBinary(operatorType, left, right, typeMapping); + return base.MakeBinary(operatorType, left, right, typeMapping, existingExpr); } /// @@ -279,7 +280,7 @@ public virtual PgNewArrayExpression NewArray( /// The right operand of binary operation. /// A type mapping to be assigned to the created expression. /// A with the given arguments. - public virtual PgBinaryExpression MakePostgresBinary( + public virtual SqlExpression MakePostgresBinary( PgExpressionType operatorType, SqlExpression left, SqlExpression right, @@ -321,7 +322,7 @@ public virtual PgBinaryExpression MakePostgresBinary( /// /// Creates a new , for checking whether one value contains another. /// - public virtual PgBinaryExpression Contains(SqlExpression left, SqlExpression right) + public virtual SqlExpression Contains(SqlExpression left, SqlExpression right) { Check.NotNull(left, nameof(left)); Check.NotNull(right, nameof(right)); @@ -332,7 +333,7 @@ public virtual PgBinaryExpression Contains(SqlExpression left, SqlExpression rig /// /// Creates a new , for checking whether one value is contained by another. /// - public virtual PgBinaryExpression ContainedBy(SqlExpression left, SqlExpression right) + public virtual SqlExpression ContainedBy(SqlExpression left, SqlExpression right) { Check.NotNull(left, nameof(left)); Check.NotNull(right, nameof(right)); @@ -343,7 +344,7 @@ public virtual PgBinaryExpression ContainedBy(SqlExpression left, SqlExpression /// /// Creates a new , for checking whether one value overlaps with another. /// - public virtual PgBinaryExpression Overlaps(SqlExpression left, SqlExpression right) + public virtual SqlExpression Overlaps(SqlExpression left, SqlExpression right) { Check.NotNull(left, nameof(left)); Check.NotNull(right, nameof(right)); @@ -475,6 +476,18 @@ private SqlBinaryExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression bina binary.Type, typeMapping ?? _typeMappingSource.FindMapping(binary.Type, "interval")); } + + // TODO: This is a hack until https://github.com/dotnet/efcore/pull/34995 is done; the translation of DateOnly.DayNumber + // generates a substraction with a fragment, but for now we can't assign a type/type mapping to a fragment. + case ExpressionType.Subtract when left.Type == typeof(DateOnly) && right is SqlFragmentExpression: + { + return new SqlBinaryExpression( + ExpressionType.Subtract, + ApplyDefaultTypeMapping(left), + right, + typeof(int), + _typeMappingSource.FindMapping(typeof(int))); + } } // If this is a row value comparison (e.g. (a, b) > (5, 6)), doing type mapping inference on each corresponding pair. @@ -495,8 +508,19 @@ private SqlBinaryExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression bina { var updatedElementBinaryExpression = MakeBinary(binary.OperatorType, leftValues[i], rightValues[i], typeMapping: null)!; - updatedLeftValues[i] = updatedElementBinaryExpression.Left; - updatedRightValues[i] = updatedElementBinaryExpression.Right; + if (updatedElementBinaryExpression is not SqlBinaryExpression + { + Left: var updatedLeft, + Right: var updatedRight, + OperatorType: var updatedOperatorType + } + || updatedOperatorType != binary.OperatorType) + { + throw new UnreachableException("MakeBinary modified binary expression type/operator when doing row value comparison"); + } + + updatedLeftValues[i] = updatedLeft; + updatedRightValues[i] = updatedRight; } // Note that we always return non-constant PostgresRowValueExpression operands, even if the original input was a @@ -540,7 +564,7 @@ bool TryGetRowValueValues(SqlExpression e, [NotNullWhen(true)] out IReadOnlyList for (var i = 0; i < v.Length; i++) { - v[i] = Constant(constantTuple[i]); + v[i] = Constant(constantTuple[i], typeof(object)); } values = v; @@ -651,7 +675,7 @@ private SqlExpression ApplyTypeMappingOnArrayIndex( // If a (non-null) type mapping is being applied, it's to the element being indexed. // Infer the array's mapping from that. var (_, array) = typeMapping is not null - ? ApplyTypeMappingsOnItemAndArray(Constant(null, typeMapping), pgArrayIndexExpression.Array) + ? ApplyTypeMappingsOnItemAndArray(Constant(null, typeMapping.ClrType, typeMapping), pgArrayIndexExpression.Array) : (null, ApplyDefaultTypeMapping(pgArrayIndexExpression.Array)); return new PgArrayIndexExpression( @@ -871,8 +895,6 @@ NpgsqlMultirangeTypeMapping multirangeTypeMapping // (e.g. IP address containment) containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping, Dependencies.Model); - // containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping); - // Apply the inferred mapping to the container, or fall back to the default type mapping if (containerMapping is not null) { @@ -1009,7 +1031,7 @@ static bool IsTextualTypeMapping(RelationalTypeMapping mapping) var newExpression = ApplyTypeMapping(expression, elementTypeMapping); if (newExpression != expression && newExpressions is null) { - newExpressions = new List(); + newExpressions = []; for (var j = 0; j < i; j++) { newExpressions.Add(pgNewArrayExpression.Expressions[j]); diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs index 52168228c..32d0b4dd7 100644 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs +++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs @@ -35,6 +35,6 @@ public override MethodCallCodeFragment GenerateUseProvider( => new( _useNpgsqlMethodInfo, providerOptions is null - ? new object[] { connectionString } - : new object[] { connectionString, new NestedClosureCodeFragment("x", providerOptions) }); + ? [connectionString] + : [connectionString, new NestedClosureCodeFragment("x", providerOptions)]); } diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs index 44580c911..4af78bfc7 100644 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs +++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs @@ -10,6 +10,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; +// ReSharper disable StringLiteralTypo + /// /// The default database model factory for Npgsql. /// @@ -17,7 +19,7 @@ public class NpgsqlDatabaseModelFactory : DatabaseModelFactory { #region Fields - private const string NamePartRegex = @"(?:(?:""(?(?:(?:"""")|[^""])+)"")|(?[^\.\[""]+))"; + private const string NamePartRegex = """(?:(?:"(?(?:(?:"")|[^"])+)")|(?[^\.\["]+))"""; private static readonly Regex SchemaTableNameExtractor = new( @@ -29,7 +31,7 @@ public class NpgsqlDatabaseModelFactory : DatabaseModelFactory RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000.0)); - private static readonly string[] SerialTypes = { "int2", "int4", "int8" }; + private static readonly string[] SerialTypes = ["int2", "int4", "int8"]; private readonly IDiagnosticsLogger _logger; @@ -228,7 +230,7 @@ deptype IN ('e', 'x') var name = reader.GetString("relname"); var type = reader.GetChar("relkind"); var comment = reader.GetValueOrDefault("description"); - var storageParameters = reader.GetValueOrDefault("reloptions") ?? Array.Empty(); + var storageParameters = reader.GetValueOrDefault("reloptions") ?? []; var table = type switch { @@ -458,7 +460,7 @@ ORDER BY attnum MaxValue = seqInfo.MaxValue, IncrementBy = (int)(seqInfo.IncrementBy ?? 1), IsCyclic = seqInfo.IsCyclic ?? false, - NumbersToCache = seqInfo.NumbersToCache ?? 1 + NumbersToCache = seqInfo.CacheSize ?? 1 }; if (!sequenceData.Equals(IdentitySequenceOptionsData.Empty)) @@ -772,7 +774,7 @@ deptype IN ('e', 'x') index[NpgsqlAnnotationNames.NullsDistinct] = false; } - foreach (var storageParameter in record.GetValueOrDefault("idx_reloptions") ?? Array.Empty()) + foreach (var storageParameter in record.GetValueOrDefault("idx_reloptions") ?? []) { if (storageParameter.Split("=") is [var paramName, var paramValue]) { @@ -839,7 +841,7 @@ deptype IN ('e', 'x') using var command = new NpgsqlCommand(commandText, connection); using var reader = command.ExecuteReader(); - constraintIndexes = new List(); + constraintIndexes = []; var tableGroups = reader.Cast().GroupBy( ddr => ( tableSchema: ddr.GetFieldValue("nspname"), @@ -976,11 +978,65 @@ private static IEnumerable GetSequences( Func? schemaFilter, IDiagnosticsLogger logger) { - // Note: we consult information_schema.sequences instead of pg_sequence but the latter was only introduced in PG 10 - var commandText = $""" + // pg_sequence was only introduced in PG 10; we prefer that (cleaner and also exposes sequence caching info), but retain the old + // code for backwards compat + return connection.PostgreSqlVersion >= new Version(10, 0) + ? GetSequencesNew(connection, databaseModel, schemaFilter, logger) + : GetSequencesOld(connection, databaseModel, schemaFilter, logger); + + static IEnumerable GetSequencesNew( + NpgsqlConnection connection, + DatabaseModel databaseModel, + Func? schemaFilter, + IDiagnosticsLogger logger) + { + var commandText = $""" +SELECT nspname, relname, typname, seqstart, seqincrement, seqmax, seqmin, seqcache, seqcycle +FROM pg_sequence +JOIN pg_class AS cls ON cls.oid=seqrelid +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +JOIN pg_type AS typ ON typ.oid = seqtypid +/* Filter out owned serial and identity sequences */ +WHERE NOT EXISTS (SELECT * FROM pg_depend AS dep WHERE dep.objid = cls.oid AND dep.deptype IN ('i', 'I', 'a')) + {(schemaFilter is not null ? $"AND {schemaFilter("nspname")}" : null)} +"""; + + using var command = new NpgsqlCommand(commandText, connection); + using var reader = command.ExecuteReader(); + + foreach (var record in reader.Cast()) + { + var sequenceSchema = reader.GetFieldValue("nspname"); + var sequenceName = reader.GetFieldValue("relname"); + + var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion); + var sequence = new DatabaseSequence + { + Database = databaseModel, + Name = sequenceName, + Schema = sequenceSchema, + StoreType = seqInfo.StoreType, + StartValue = seqInfo.StartValue, + MinValue = seqInfo.MinValue, + MaxValue = seqInfo.MaxValue, + IncrementBy = (int?)seqInfo.IncrementBy, + IsCyclic = seqInfo.IsCyclic, + }; + + yield return sequence; + } + } + + static IEnumerable GetSequencesOld( + NpgsqlConnection connection, + DatabaseModel databaseModel, + Func? schemaFilter, + IDiagnosticsLogger logger) + { + var commandText = $""" SELECT sequence_schema, sequence_name, - data_type AS seqtype, + data_type AS typname, {(connection.PostgreSqlVersion >= new Version(9, 1) ? "start_value" : "1")}::bigint AS seqstart, minimum_value::bigint AS seqmin, maximum_value::bigint AS seqmax, @@ -1001,29 +1057,30 @@ AND NOT EXISTS (SELECT * FROM pg_depend AS dep WHERE dep.objid = cls.oid AND dep {(schemaFilter is not null ? $"AND {schemaFilter("nspname")}" : null)} """; - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - - foreach (var record in reader.Cast()) - { - var sequenceName = reader.GetFieldValue("sequence_name"); - var sequenceSchema = reader.GetFieldValue("sequence_schema"); + using var command = new NpgsqlCommand(commandText, connection); + using var reader = command.ExecuteReader(); - var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion); - var sequence = new DatabaseSequence + foreach (var record in reader.Cast()) { - Database = databaseModel, - Name = sequenceName, - Schema = sequenceSchema, - StoreType = seqInfo.StoreType, - StartValue = seqInfo.StartValue, - MinValue = seqInfo.MinValue, - MaxValue = seqInfo.MaxValue, - IncrementBy = (int?)seqInfo.IncrementBy, - IsCyclic = seqInfo.IsCyclic - }; - - yield return sequence; + var sequenceName = reader.GetFieldValue("sequence_name"); + var sequenceSchema = reader.GetFieldValue("sequence_schema"); + + var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion); + var sequence = new DatabaseSequence + { + Database = databaseModel, + Name = sequenceName, + Schema = sequenceSchema, + StoreType = seqInfo.StoreType, + StartValue = seqInfo.StartValue, + MinValue = seqInfo.MinValue, + MaxValue = seqInfo.MaxValue, + IncrementBy = (int?)seqInfo.IncrementBy, + IsCyclic = seqInfo.IsCyclic + }; + + yield return sequence; + } } } @@ -1110,7 +1167,11 @@ private static void GetCollations( var commandText = $""" SELECT nspname, collname, collprovider, collcollate, collctype, - {(connection.PostgreSqlVersion >= new Version(15, 0) ? "colliculocale" : "NULL AS colliculocale")}, + {(connection.PostgreSqlVersion.Major switch { + >= 17 => "colllocale", + >= 15 => "colliculocale AS colllocale", + _ => "NULL AS colllocale" + })}, {(connection.PostgreSqlVersion >= new Version(12, 0) ? "collisdeterministic" : "true AS collisdeterministic")} FROM pg_collation coll JOIN pg_namespace ns ON ns.oid=coll.collnamespace @@ -1126,7 +1187,7 @@ nspname NOT IN ({internalSchemas}) { var schema = reader.GetString(reader.GetOrdinal("nspname")); var name = reader.GetString(reader.GetOrdinal("collname")); - var icuLocale = reader.GetValueOrDefault("colliculocale"); + var icuLocale = reader.GetValueOrDefault("colllocale"); var lcCollate = reader.GetValueOrDefault("collcollate"); var lcCtype = reader.GetValueOrDefault("collctype"); var providerCode = reader.GetChar(reader.GetOrdinal("collprovider")); @@ -1225,16 +1286,24 @@ private static void AdjustDefaults(DatabaseColumn column, string systemTypeName) private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgresVersion) { - var storeType = record.GetFieldValue("seqtype"); + var storeType = record.GetFieldValue("typname"); var startValue = record.GetValueOrDefault("seqstart"); var minValue = record.GetValueOrDefault("seqmin"); var maxValue = record.GetValueOrDefault("seqmax"); var incrementBy = record.GetValueOrDefault("seqincrement"); var isCyclic = record.GetValueOrDefault("seqcycle"); - var numbersToCache = (int)record.GetValueOrDefault("seqcache"); + var cacheSize = (int?)record.GetValueOrDefault("seqcache"); long defaultStart, defaultMin, defaultMax; + storeType = storeType switch + { + "int2" => "smallint", + "int4" => "integer", + "int8" => "bigint", + _ => storeType + }; + switch (storeType) { case "smallint" when incrementBy > 0: @@ -1293,24 +1362,19 @@ private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgr MaxValue = maxValue == defaultMax ? null : maxValue, IncrementBy = incrementBy == 1 ? null : incrementBy, IsCyclic = isCyclic == false ? null : true, - NumbersToCache = numbersToCache == 1 ? null : numbersToCache + CacheSize = cacheSize is 1 or null ? null : cacheSize }; } - private sealed class SequenceInfo + private sealed class SequenceInfo(string storeType) { - public SequenceInfo(string storeType) - { - StoreType = storeType; - } - - public string StoreType { get; } + public string StoreType { get; } = storeType; public long? StartValue { get; set; } public long? MinValue { get; set; } public long? MaxValue { get; set; } public long? IncrementBy { get; set; } public bool? IsCyclic { get; set; } - public long? NumbersToCache { get; set; } + public int? CacheSize { get; set; } } #endregion diff --git a/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs index 6e8e74dfb..f8f62958f 100644 --- a/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs +++ b/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs @@ -1,4 +1,6 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using System.Data.Common; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -8,6 +10,14 @@ /// public interface INpgsqlRelationalConnection : IRelationalConnection { + /// + /// 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. + /// + DbDataSource? DataSource { 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 @@ -22,5 +32,5 @@ public interface INpgsqlRelationalConnection : IRelationalConnection /// 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. /// - NpgsqlRelationalConnection CloneWith(string connectionString); + ValueTask CloneWith(string connectionString, bool async, CancellationToken cancellationToken = default); } diff --git a/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs b/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs index f74691d7e..eeb293210 100644 --- a/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs +++ b/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs @@ -12,6 +12,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; /// public sealed class JsonBitArrayReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonBitArrayReaderWriter).GetProperty(nameof(Instance))!; + /// /// 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 @@ -51,4 +53,7 @@ public override void ToJsonTyped(Utf8JsonWriter writer, BitArray value) s[i] = a[i] ? '1' : '0'; } })); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } diff --git a/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs b/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs index d2a9d4aed..ca1aa2bc6 100644 --- a/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs +++ b/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs @@ -12,6 +12,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; /// public sealed class JsonMacaddrReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonMacaddrReaderWriter).GetProperty(nameof(Instance))!; + /// /// 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 @@ -31,4 +33,7 @@ public override PhysicalAddress FromJsonTyped(ref Utf8JsonReaderManager manager, /// public override void ToJsonTyped(Utf8JsonWriter writer, PhysicalAddress value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs index 3ae21fcec..a336b1148 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs @@ -133,18 +133,19 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType ? null // TODO: Value comparer for multidimensional arrays : (ValueComparer?)Activator.CreateInstance( elementType.IsNullableValueType() - ? typeof(NullableValueTypeListComparer<>).MakeGenericType(elementType.UnwrapNullableType()) - : elementMapping.Comparer.Type.IsAssignableFrom(elementType) - ? typeof(ListComparer<>).MakeGenericType(elementType) - : typeof(ObjectListComparer<>).MakeGenericType(elementType), + ? typeof(ListOfNullableValueTypesComparer<,>) + .MakeGenericType(typeof(TConcreteCollection), elementType.UnwrapNullableType()) + : elementType.IsValueType + ? typeof(ListOfValueTypesComparer<,>).MakeGenericType(typeof(TConcreteCollection), elementType) + : typeof(ListOfReferenceTypesComparer<,>).MakeGenericType(typeof(TConcreteCollection), elementType), elementMapping.Comparer.ToNullableComparer(elementType)!); #pragma warning restore EF1001 var elementJsonReaderWriter = elementMapping.JsonValueReaderWriter; - if (elementJsonReaderWriter is not null && elementJsonReaderWriter.ValueType != typeof(TElement).UnwrapNullableType()) + if (elementJsonReaderWriter is not null && !typeof(TElement).UnwrapNullableType().IsAssignableTo(elementJsonReaderWriter.ValueType)) { throw new InvalidOperationException( - $"When '{elementJsonReaderWriter.ValueType}', '{typeof(TElement).UnwrapNullableType()}' building an array mapping, the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementMapping.JsonValueReaderWriter?.GetType().Name ?? ""}')."); + $"When building an array mapping over '{typeof(TElement).Name}', the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementJsonReaderWriter.ValueType.GetType().Name}' instead of '{typeof(TElement).UnwrapNullableType()}', the JsonValueReaderWriter is '{elementJsonReaderWriter.GetType().Name}')."); } // If there's no JsonValueReaderWriter on the element, we also don't set one on its array (this is for rare edge cases such as @@ -155,9 +156,11 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType ? null : (JsonValueReaderWriter?)Activator.CreateInstance( (elementType.IsNullableValueType() - ? typeof(JsonNullableStructCollectionReaderWriter<,,>) - : typeof(JsonCollectionReaderWriter<,,>)) - .MakeGenericType(typeof(TCollection), typeof(TConcreteCollection), elementType.UnwrapNullableType()), + ? typeof(JsonCollectionOfNullableStructsReaderWriter<,>) + : elementType.IsValueType + ? typeof(JsonCollectionOfStructsReaderWriter<,>) + : typeof(JsonCollectionOfReferencesReaderWriter<,>)) + .MakeGenericType(typeof(TConcreteCollection), elementType.UnwrapNullableType()), elementJsonReaderWriter); return new RelationalTypeMappingParameters( @@ -187,7 +190,7 @@ protected NpgsqlArrayTypeMapping(RelationalTypeMappingParameters parameters) // Otherwise let the ADO.NET layer infer the PostgreSQL type. We can't always let it infer, otherwise // when given a byte[] it will infer byte (but we want smallint[]) NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Array - | (ElementTypeMapping is INpgsqlTypeMapping elementNpgsqlTypeMapping + | (ElementTypeMapping is INpgsqlTypeMapping { NpgsqlDbType: not NpgsqlTypes.NpgsqlDbType.Unknown } elementNpgsqlTypeMapping ? elementNpgsqlTypeMapping.NpgsqlDbType : ElementTypeMapping.DbType.HasValue ? new NpgsqlParameter { DbType = ElementTypeMapping.DbType.Value }.NpgsqlDbType @@ -219,8 +222,10 @@ public override DbParameter CreateParameter( // In queries which compose non-server-correlated LINQ operators over an array parameter (e.g. Where(b => ids.Skip(1)...) we // get an enumerable parameter value that isn't an array/list - but those aren't supported at the Npgsql ADO level. // Detect this here and evaluate the enumerable to get a fully materialized List. + // Note that when we have a value converter (e.g. for HashSet), we don't want to convert it to a List, since the value converter + // expects the original type. // TODO: Make Npgsql support IList<> instead of only arrays and List<> - if (value is not null && !value.GetType().IsArrayOrGenericList()) + if (value is not null && Converter is null && !value.GetType().IsArrayOrGenericList()) { switch (value) { @@ -234,7 +239,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/NpgsqlBigIntegerTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs index 2346b5b1c..4be7eb2f0 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs @@ -64,15 +64,44 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param ? $"numeric({parameters.Precision})" : $"numeric({parameters.Precision},{parameters.Scale})"; - private sealed class JsonBigIntegerReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonBigIntegerReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonBigIntegerReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonBigIntegerReaderWriter Instance { get; } = new(); + /// + /// 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. + /// // Other systems handling the JSON very likely won't support arbitrary-length numbers here, we encode as a string public override BigInteger FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => BigInteger.Parse(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, BigInteger value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBitTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBitTypeMapping.cs index f2e148e71..977fbd56e 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBitTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBitTypeMapping.cs @@ -87,5 +87,5 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(BitArray).GetConstructor(new[] { typeof(bool[]) })!; + typeof(BitArray).GetConstructor([typeof(bool[])])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs index ca0a3095e..5df6a7219 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs @@ -122,7 +122,13 @@ protected override void ConfigureParameter(DbParameter parameter) base.ConfigureParameter(parameter); } - private static bool EqualsWithoutTrailingWhitespace(string? a, string? b) + /// + /// 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 bool EqualsWithoutTrailingWhitespace(string? a, string? b) => (a, b) switch { (null, null) => true, @@ -131,6 +137,12 @@ private static bool EqualsWithoutTrailingWhitespace(string? a, string? b) _ => a.AsSpan().TrimEnd().SequenceEqual(b.AsSpan().TrimEnd()) }; - private static int GetHashCodeWithoutTrailingWhitespace(string a) + /// + /// 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 int GetHashCodeWithoutTrailingWhitespace(string a) => a.TrimEnd().GetHashCode(); } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs index e9e2d0b84..7a943d887 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs @@ -78,19 +78,48 @@ public override Expression GenerateCodeLiteral(object value) Expression.Constant(cidr.Netmask)); } - private static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", new[] { typeof(string) })!; + private static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; private static readonly ConstructorInfo NpgsqlCidrConstructor = - typeof(NpgsqlCidr).GetConstructor(new[] { typeof(IPAddress), typeof(byte) })!; + typeof(NpgsqlCidr).GetConstructor([typeof(IPAddress), typeof(byte)])!; - private sealed class JsonCidrReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonCidrReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonCidrReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonCidrReaderWriter Instance { get; } = new(); + /// + /// 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 override NpgsqlCidr FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => new(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, NpgsqlCidr value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs index 58015300c..3b6be8c3b 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs @@ -87,10 +87,30 @@ private static string Format(DateOnly date) return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } - private sealed class NpgsqlJsonDateOnlyReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonDateOnlyReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonDateOnlyReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonDateOnlyReaderWriter Instance { get; } = new(); + /// + /// 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 override DateOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) { var s = manager.CurrentReader.GetString()!; @@ -109,7 +129,16 @@ public override DateOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object return DateOnly.Parse(s, CultureInfo.InvariantCulture); } + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateOnly value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs index 83095faf0..c966b483b 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs @@ -87,10 +87,30 @@ private static string Format(DateTime date) return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } - private sealed class NpgsqlJsonDateTimeReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonDateTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonDateTimeReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonDateTimeReaderWriter Instance { get; } = new(); + /// + /// 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 override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) { var s = manager.CurrentReader.GetString()!; @@ -109,7 +129,16 @@ public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object return DateTime.Parse(s, CultureInfo.InvariantCulture); } + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs index 94f1ead19..108892bb3 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 /// @@ -124,6 +150,16 @@ private static Dictionary CreateValueMapping(Type enumType, INpg public sealed class JsonPgEnumReaderWriter : JsonValueReaderWriter where T : struct, Enum { + private static readonly PropertyInfo InstanceProperty = typeof(JsonPgEnumReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonPgEnumReaderWriter Instance { get; } = new(); + /// /// 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 @@ -141,5 +177,8 @@ public override T FromJsonTyped(ref Utf8JsonReaderManager manager, object? exist /// public override void ToJsonTyped(Utf8JsonWriter writer, T value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs index d7df0b599..d93451429 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs @@ -75,7 +75,7 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPoint).GetConstructor(new[] { typeof(double), typeof(double) })!; + typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; } /// @@ -155,7 +155,7 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLine).GetConstructor(new[] { typeof(double), typeof(double), typeof(double) })!; + typeof(NpgsqlLine).GetConstructor([typeof(double), typeof(double), typeof(double)])!; } /// @@ -233,7 +233,7 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLSeg).GetConstructor(new[] { typeof(double), typeof(double), typeof(double), typeof(double) })!; + typeof(NpgsqlLSeg).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; } /// @@ -311,7 +311,7 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlBox).GetConstructor(new[] { typeof(double), typeof(double), typeof(double), typeof(double) })!; + typeof(NpgsqlBox).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; } /// @@ -412,10 +412,10 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPath).GetConstructor(new[] { typeof(IEnumerable), typeof(bool) })!; + typeof(NpgsqlPath).GetConstructor([typeof(IEnumerable), typeof(bool)])!; private static readonly ConstructorInfo PointConstructor = - typeof(NpgsqlPoint).GetConstructor(new[] { typeof(double), typeof(double) })!; + typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; } /// @@ -513,10 +513,10 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPolygon).GetConstructor(new[] { typeof(NpgsqlPoint[]) })!; + typeof(NpgsqlPolygon).GetConstructor([typeof(NpgsqlPoint[])])!; private static readonly ConstructorInfo PointConstructor = - typeof(NpgsqlPoint).GetConstructor(new[] { typeof(double), typeof(double) })!; + typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; } /// @@ -593,5 +593,5 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlCircle).GetConstructor(new[] { typeof(double), typeof(double), typeof(double) })!; + typeof(NpgsqlCircle).GetConstructor([typeof(double), typeof(double), typeof(double)])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs index d48c3e121..3d6eef2f0 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs @@ -109,16 +109,11 @@ protected override string GenerateNonNullSqlLiteral(object value) $"CLR type must be {nameof(Dictionary)} or {nameof(ImmutableDictionary)}"); } - private sealed class HstoreMutableComparer : ValueComparer> + private sealed class HstoreMutableComparer() : ValueComparer>( + (a, b) => Compare(a, b), + o => o.GetHashCode(), + o => new Dictionary(o)) { - public HstoreMutableComparer() - : base( - (a, b) => Compare(a, b), - o => o.GetHashCode(), - o => new Dictionary(o)) - { - } - private static bool Compare(Dictionary? a, Dictionary? b) { if (a is null) diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs index ab88eb5fb..ad99bd3c6 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs @@ -82,28 +82,86 @@ public override Expression GenerateCodeLiteral(object value) _ => throw new UnreachableException() }; - private static readonly MethodInfo IPAddressParseMethod = typeof(IPAddress).GetMethod("Parse", new[] { typeof(string) })!; - private static readonly ConstructorInfo NpgsqlInetConstructor = typeof(NpgsqlInet).GetConstructor(new[] { typeof(string) })!; + private static readonly MethodInfo IPAddressParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; + private static readonly ConstructorInfo NpgsqlInetConstructor = typeof(NpgsqlInet).GetConstructor([typeof(string)])!; - private sealed class JsonIPAddressReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonIPAddressReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonIPAddressReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonIPAddressReaderWriter Instance { get; } = new(); + /// + /// 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 override IPAddress FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => IPAddress.Parse(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, IPAddress value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } - private sealed class JsonNpgsqlInetReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonNpgsqlInetReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonNpgsqlInetReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonNpgsqlInetReaderWriter Instance { get; } = new(); + /// + /// 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 override NpgsqlInet FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => new(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, NpgsqlInet value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs index 23f06d5db..7bf603944 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs @@ -79,7 +79,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{FormatTimeSpanAsInterval((TimeSpan)value)}"""; + => $""" + "{FormatTimeSpanAsInterval((TimeSpan)value)}" + """; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -155,14 +157,43 @@ public static TimeSpan ParseIntervalAsTimeSpan(ReadOnlySpan s) return timeSpan; } - private sealed class NpgsqlJsonTimeSpanReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonTimeSpanReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonTimeSpanReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonTimeSpanReaderWriter Instance { get; } = new(); + /// + /// 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 override TimeSpan FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, TimeSpan value) => writer.WriteStringValue(FormatTimeSpanAsInterval(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs index dcc449191..e0931b198 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs @@ -126,5 +126,5 @@ public override Expression GenerateCodeLiteral(object value) private static readonly Expression DefaultJsonDocumentOptions = Expression.New(typeof(JsonDocumentOptions)); private static readonly MethodInfo ParseMethod = - typeof(JsonDocument).GetMethod(nameof(JsonDocument.Parse), new[] { typeof(string), typeof(JsonDocumentOptions) })!; + typeof(JsonDocument).GetMethod(nameof(JsonDocument.Parse), [typeof(string), typeof(JsonDocumentOptions)])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlLTreeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlLTreeTypeMapping.cs index d54529bc0..8f31c436f 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlLTreeTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlLTreeTypeMapping.cs @@ -11,7 +11,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// public class NpgsqlLTreeTypeMapping : NpgsqlStringTypeMapping { - private static readonly ConstructorInfo Constructor = typeof(LTree).GetConstructor(new[] { typeof(string) })!; + private static readonly ConstructorInfo Constructor = typeof(LTree).GetConstructor([typeof(string)])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -68,14 +68,43 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p public override Expression GenerateCodeLiteral(object value) => Expression.New(Constructor, Expression.Constant((string)(LTree)value, typeof(string))); - private sealed class JsonLTreeReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonLTreeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonLTreeReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonLTreeReaderWriter Instance { get; } = new(); + /// + /// 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 override LTree FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => manager.CurrentReader.GetString()!; + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, LTree value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddr8TypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddr8TypeMapping.cs index 61bbc1897..c73b20642 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddr8TypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddr8TypeMapping.cs @@ -72,5 +72,5 @@ protected override string GenerateNonNullSqlLiteral(object value) public override Expression GenerateCodeLiteral(object value) => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); - private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", new[] { typeof(string) })!; + private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", [typeof(string)])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddrTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddrTypeMapping.cs index 961cfee74..25ccc25bd 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddrTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddrTypeMapping.cs @@ -68,5 +68,5 @@ protected override string GenerateNonNullSqlLiteral(object value) public override Expression GenerateCodeLiteral(object value) => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); - private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", new[] { typeof(string) })!; + private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", [typeof(string)])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlOwnedJsonTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlOwnedJsonTypeMapping.cs index 0a0c935fc..0be732a0d 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlOwnedJsonTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlOwnedJsonTypeMapping.cs @@ -17,16 +17,16 @@ public class NpgsqlOwnedJsonTypeMapping : JsonTypeMapping public virtual NpgsqlDbType NpgsqlDbType { get; } private static readonly MethodInfo GetStringMethod - = typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) })!; + = typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), [typeof(int)])!; private static readonly PropertyInfo UTF8Property = typeof(Encoding).GetProperty(nameof(Encoding.UTF8))!; private static readonly MethodInfo EncodingGetBytesMethod - = typeof(Encoding).GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) })!; + = typeof(Encoding).GetMethod(nameof(Encoding.GetBytes), [typeof(string)])!; private static readonly ConstructorInfo MemoryStreamConstructor - = typeof(MemoryStream).GetConstructor(new[] { typeof(byte[]) })!; + = typeof(MemoryStream).GetConstructor([typeof(byte[])])!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs index 64345aca4..f107520cb 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs @@ -81,20 +81,49 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLogSequenceNumber).GetConstructor(new[] { typeof(ulong) })!; + typeof(NpgsqlLogSequenceNumber).GetConstructor([typeof(ulong)])!; - private sealed class JsonLogSequenceNumberReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonLogSequenceNumberReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonLogSequenceNumberReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 JsonLogSequenceNumberReaderWriter Instance { get; } = new(); private JsonLogSequenceNumberReaderWriter() { } + /// + /// 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 override NpgsqlLogSequenceNumber FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => NpgsqlLogSequenceNumber.Parse(manager.CurrentReader.GetString()!); + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, NpgsqlLogSequenceNumber value) => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs index 7687410d6..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; } /// @@ -260,10 +260,10 @@ private void InitializeAccessors(Type rangeClrType, Type subtypeClrType) _upperInfiniteProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.UpperBoundInfinite))!; _rangeConstructor1 = rangeClrType.GetConstructor( - new[] { subtypeClrType, subtypeClrType })!; + [subtypeClrType, subtypeClrType])!; _rangeConstructor2 = rangeClrType.GetConstructor( - new[] { subtypeClrType, typeof(bool), subtypeClrType, typeof(bool) })!; + [subtypeClrType, typeof(bool), subtypeClrType, typeof(bool)])!; _rangeConstructor3 = rangeClrType.GetConstructor( - new[] { subtypeClrType, typeof(bool), typeof(bool), subtypeClrType, typeof(bool), typeof(bool) })!; + [subtypeClrType, typeof(bool), typeof(bool), subtypeClrType, typeof(bool), typeof(bool)])!; } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs index d4185164e..bcf13e7b4 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs @@ -79,5 +79,5 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(NpgsqlTid).GetConstructor(new[] { typeof(uint), typeof(ushort) })!; + typeof(NpgsqlTid).GetConstructor([typeof(uint), typeof(ushort)])!; } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs index 1ed301f1e..fad0f4143 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs @@ -77,8 +77,16 @@ protected override string GenerateNonNullSqlLiteral(object value) protected override string GenerateEmbeddedNonNullSqlLiteral(object value) => FormattableString.Invariant(@$"{(DateTimeOffset)value:HH:mm:ss.FFFFFFz}"); - private sealed class JsonTimeTzReaderWriter : JsonValueReaderWriter + /// + /// 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 JsonTimeTzReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(JsonTimeTzReaderWriter).GetProperty(nameof(Instance))!; + /// /// 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 @@ -91,12 +99,25 @@ private JsonTimeTzReaderWriter() { } - /// + /// + /// 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 override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => DateTimeOffset.Parse(manager.CurrentReader.GetString()!); - /// + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateTimeOffset value) => writer.WriteStringValue(value.ToString("HH:mm:ss.FFFFFFz")); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs index 38ebc6aeb..21e3fccf0 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs @@ -76,7 +76,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $""" + "{GenerateLiteralCore(value)}" + """; private string GenerateLiteralCore(object value) => FormatDateTime((DateTime)value); @@ -101,10 +103,30 @@ private static string FormatDateTime(DateTime dateTime) : throw new ArgumentException("'timestamp without time zone' literal cannot be generated for a UTC DateTime"); } - private sealed class NpgsqlJsonTimestampReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonTimestampReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonTimestampReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonTimestampReaderWriter Instance { get; } = new(); + /// + /// 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 override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) { var s = manager.CurrentReader.GetString()!; @@ -123,7 +145,16 @@ public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object return DateTime.Parse(s, CultureInfo.InvariantCulture); } + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) => writer.WriteStringValue(FormatDateTime(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs index 8a467a511..ef1ace200 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs @@ -84,7 +84,9 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => @$"""{Format(value)}"""; + => $""" + "{Format(value)}" + """; private static string Format(object value) => value switch @@ -146,10 +148,31 @@ private static string Format(DateTimeOffset dateTimeOffset) return dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture); } - private sealed class NpgsqlJsonTimestampTzDateTimeReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonTimestampTzDateTimeReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty + = typeof(NpgsqlJsonTimestampTzDateTimeReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonTimestampTzDateTimeReaderWriter Instance { get; } = new(); + /// + /// 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 override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) { var s = manager.CurrentReader.GetString()!; @@ -170,14 +193,44 @@ public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object return DateTime.Parse(s, CultureInfo.InvariantCulture).ToUniversalTime(); } + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } - private sealed class NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter : JsonValueReaderWriter + /// + /// 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 NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter : JsonValueReaderWriter { + private static readonly PropertyInfo InstanceProperty + = typeof(NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter Instance { get; } = new(); + /// + /// 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 override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) { var s = manager.CurrentReader.GetString()!; @@ -196,7 +249,16 @@ public override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager, return DateTimeOffset.Parse(s, CultureInfo.InvariantCulture); } + /// + /// 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 override void ToJsonTyped(Utf8JsonWriter writer, DateTimeOffset value) => writer.WriteStringValue(Format(value)); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs index c24064b99..6b6028672 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs @@ -10,6 +10,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// public class NpgsqlUIntTypeMapping : NpgsqlTypeMapping { + /// + /// 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 NpgsqlUIntTypeMapping Default { get; } = new("xid", NpgsqlDbType.Xid); + /// /// 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/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs index 4f024940d..909475cc2 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs @@ -92,5 +92,5 @@ public override Expression GenerateCodeLiteral(object value) } private static readonly ConstructorInfo Constructor = - typeof(BitArray).GetConstructor(new[] { typeof(bool[]) })!; + typeof(BitArray).GetConstructor([typeof(bool[])])!; } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs new file mode 100644 index 000000000..bd67b7689 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs @@ -0,0 +1,194 @@ +using System.Collections.Concurrent; +using System.Data.Common; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.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 } + => npgsqlOptionsExtension.DataSourceBuilderAction is null + ? dataSource + // If the user has explicitly passed in a data source via UseNpgsql(), but also supplied a data source configuration + // lambda, throw - we're unable to apply the configuration lambda to the externally-provided, already-built data source. + : throw new NotSupportedException(NpgsqlStrings.DataSourceAndConfigNotSupported), + + // 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. + { DataSourceBuilderAction: not null } => GetSingletonDataSource(npgsqlOptionsExtension), + { 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 + || npgsqlOptionsExtension.RemoteCertificateValidationCallback is not null) + { + dataSourceBuilder.UseSslClientAuthenticationOptionsCallback(o => + { + if (npgsqlOptionsExtension.ProvideClientCertificatesCallback is not null) + { + o.ClientCertificates ??= new(); + npgsqlOptionsExtension.ProvideClientCertificatesCallback(o.ClientCertificates); + } + + o.RemoteCertificateValidationCallback = npgsqlOptionsExtension.RemoteCertificateValidationCallback; + }); + } + + // Finally, if the user has provided a data source builder configuration action, invoke it. + // Do this last, to allow the user to override anything set above. + npgsqlOptionsExtension.DataSourceBuilderAction?.Invoke(dataSourceBuilder); + + 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/NpgsqlDatabaseCreator.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs index b4fc8190a..4034f5ef2 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs @@ -10,27 +10,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// 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 NpgsqlDatabaseCreator : RelationalDatabaseCreator -{ - private readonly INpgsqlRelationalConnection _connection; - private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; - - /// - /// 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 NpgsqlDatabaseCreator( +public class NpgsqlDatabaseCreator( RelationalDatabaseCreatorDependencies dependencies, INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : base(dependencies) - { - _connection = connection; - _rawSqlCommandBuilder = rawSqlCommandBuilder; - } - + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalConnectionDiagnosticsLogger connectionLogger) + : RelationalDatabaseCreator(dependencies) +{ /// /// 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 @@ -55,7 +41,7 @@ public NpgsqlDatabaseCreator( /// public override void Create() { - using (var masterConnection = _connection.CreateAdminConnection()) + using (var masterConnection = connection.CreateAdminConnection()) { try { @@ -82,7 +68,7 @@ public override void Create() /// public override async Task CreateAsync(CancellationToken cancellationToken = default) { - var masterConnection = _connection.CreateAdminConnection(); + var masterConnection = connection.CreateAdminConnection(); await using (masterConnection.ConfigureAwait(false)) { try @@ -112,7 +98,7 @@ await Dependencies.MigrationCommandExecutor public override bool HasTables() => Dependencies.ExecutionStrategy .Execute( - _connection, + connection, connection => (bool)CreateHasTablesCommand() .ExecuteScalar( new RelationalCommandParameterObject( @@ -130,7 +116,7 @@ public override bool HasTables() /// public override Task HasTablesAsync(CancellationToken cancellationToken = default) => Dependencies.ExecutionStrategy.ExecuteAsync( - _connection, + connection, async (connection, ct) => (bool)(await CreateHasTablesCommand() .ExecuteScalarAsync( new RelationalCommandParameterObject( @@ -142,7 +128,7 @@ public override Task HasTablesAsync(CancellationToken cancellationToken = cancellationToken: ct).ConfigureAwait(false))!, cancellationToken); private IRelationalCommand CreateHasTablesCommand() - => _rawSqlCommandBuilder + => rawSqlCommandBuilder .Build( """ SELECT CASE WHEN COUNT(*) = 0 THEN FALSE ELSE TRUE END @@ -170,16 +156,15 @@ private IReadOnlyList CreateCreateOperations() var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; return Dependencies.MigrationsSqlGenerator.Generate( - new[] - { - new NpgsqlCreateDatabaseOperation + [ + new NpgsqlCreateDatabaseOperation { - Name = _connection.DbConnection.Database, + Name = connection.DbConnection.Database, Template = designTimeModel.GetDatabaseTemplate(), Collation = designTimeModel.GetCollation(), Tablespace = designTimeModel.GetTablespace() } - }); + ]); } /// @@ -202,14 +187,30 @@ public override Task ExistsAsync(CancellationToken cancellationToken = def private async Task Exists(bool async, CancellationToken cancellationToken = default) { + var logger = connectionLogger; + var startTime = DateTimeOffset.UtcNow; + + var interceptionResult = async + ? await logger.ConnectionOpeningAsync(connection, startTime, cancellationToken).ConfigureAwait(false) + : logger.ConnectionOpening(connection, startTime); + + if (interceptionResult.IsSuppressed) + { + // If the connection attempt was suppressed by an interceptor, assume that the interceptor took care of all the opening + // details, and the database exists. + return true; + } + // When checking whether a database exists, pooling must be off, otherwise we may // attempt to reuse a pooled connection, which may be broken (this happened in the tests). // If Pooling is off, but Multiplexing is on - NpgsqlConnectionStringBuilder.Validate will throw, // so we turn off Multiplexing as well. - var unpooledCsb = new NpgsqlConnectionStringBuilder(_connection.ConnectionString) { Pooling = false, Multiplexing = false }; + var unpooledCsb = new NpgsqlConnectionStringBuilder(connection.ConnectionString) { Pooling = false, Multiplexing = false }; using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - var unpooledRelationalConnection = _connection.CloneWith(unpooledCsb.ToString()); + var unpooledRelationalConnection = + await connection.CloneWith(unpooledCsb.ToString(), async, cancellationToken).ConfigureAwait(false); + try { if (async) @@ -272,9 +273,17 @@ private static bool IsDoesNotExist(PostgresException exception) /// public override void Delete() { - ClearAllPools(); + switch (connection.DataSource) + { + case NpgsqlDataSource dataSource: + dataSource.Clear(); + break; + case null: + ClearAllPools(); + break; + } - using (var masterConnection = _connection.CreateAdminConnection()) + using (var masterConnection = connection.CreateAdminConnection()) { Dependencies.MigrationCommandExecutor .ExecuteNonQuery(CreateDropCommands(), masterConnection); @@ -289,9 +298,18 @@ public override void Delete() /// public override async Task DeleteAsync(CancellationToken cancellationToken = default) { - ClearAllPools(); + switch (connection.DataSource) + { + case NpgsqlDataSource dataSource: + // TODO: Do this asynchronously once https://github.com/npgsql/npgsql/issues/4499 is done + dataSource.Clear(); + break; + case null: + ClearAllPools(); + break; + } - var masterConnection = _connection.CreateAdminConnection(); + var masterConnection = connection.CreateAdminConnection(); await using (masterConnection) { await Dependencies.MigrationCommandExecutor @@ -321,7 +339,7 @@ public override void CreateTables() try { - Dependencies.MigrationCommandExecutor.ExecuteNonQuery(commands, _connection); + Dependencies.MigrationCommandExecutor.ExecuteNonQuery(commands, connection); } catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) { @@ -329,7 +347,7 @@ public override void CreateTables() // (happens in the tests). Simply ignore the error. } - if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) + if (reloadTypes && connection.DbConnection is NpgsqlConnection npgsqlConnection) { npgsqlConnection.Open(); try @@ -357,7 +375,7 @@ public override async Task CreateTablesAsync(CancellationToken cancellationToken try { - await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(commands, _connection, cancellationToken) + await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(commands, connection, cancellationToken) .ConfigureAwait(false); } catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) @@ -371,12 +389,12 @@ await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(commands, _conn .OfType() .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); - if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) + if (reloadTypes && connection.DbConnection is NpgsqlConnection npgsqlConnection) { await npgsqlConnection.OpenAsync(cancellationToken).ConfigureAwait(false); try { - await npgsqlConnection.ReloadTypesAsync().ConfigureAwait(false); + await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); } catch { @@ -391,7 +409,7 @@ private IReadOnlyList CreateDropCommands() { // TODO Check DbConnection.Database always gives us what we want // Issue #775 - new NpgsqlDropDatabaseOperation { Name = _connection.DbConnection.Database } + new NpgsqlDropDatabaseOperation { Name = connection.DbConnection.Database } }; return Dependencies.MigrationsSqlGenerator.Generate(operations); @@ -404,5 +422,5 @@ private static void ClearAllPools() // Clear connection pool for the database connection since after the 'create database' call, a previously // invalid connection may now be valid. private void ClearPool() - => NpgsqlConnection.ClearPool((NpgsqlConnection)_connection.DbConnection); + => NpgsqlConnection.ClearPool((NpgsqlConnection)connection.DbConnection); } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs index 79e651096..89df883b0 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs @@ -21,7 +21,13 @@ public class NpgsqlRelationalConnection : RelationalConnection, INpgsqlRelationa private readonly ProvidePasswordCallback? _providePasswordCallback; #pragma warning restore CS0618 - private DbDataSource? _dataSource; + /// + /// 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 DbDataSource? DataSource { get; private set; } /// /// Indicates whether the store connection supports ambient transactions @@ -35,8 +41,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)) { } @@ -51,7 +64,7 @@ protected NpgsqlRelationalConnection(RelationalConnectionDependencies dependenci { if (dataSource is not null) { - _dataSource = dataSource; + DataSource = dataSource; #if DEBUG // We validate in NpgsqlOptionsExtensions.Validate that DataSource and these callbacks aren't specified together @@ -85,21 +98,25 @@ protected NpgsqlRelationalConnection(RelationalConnectionDependencies dependenci /// protected override DbConnection CreateDbConnection() { - if (_dataSource is not null) + if (DataSource is not null) { - return _dataSource.CreateConnection(); + return DataSource.CreateConnection(); } var conn = new NpgsqlConnection(ConnectionString); - if (_provideClientCertificatesCallback is not null) + if (_provideClientCertificatesCallback is not null || _remoteCertificateValidationCallback is not null) { - conn.ProvideClientCertificatesCallback = _provideClientCertificatesCallback; - } - - if (_remoteCertificateValidationCallback is not null) - { - conn.UserCertificateValidationCallback = _remoteCertificateValidationCallback; + conn.SslClientAuthenticationOptionsCallback = o => + { + if (_provideClientCertificatesCallback is not null) + { + o.ClientCertificates ??= new(); + _provideClientCertificatesCallback(o.ClientCertificates); + } + + o.RemoteCertificateValidationCallback = _remoteCertificateValidationCallback; + }; } if (_providePasswordCallback is not null) @@ -121,12 +138,12 @@ protected override DbConnection CreateDbConnection() // TODO: Remove after DbDataSource support is added to EF Core (https://github.com/dotnet/efcore/issues/28266) public override string? ConnectionString { - get => _dataSource is null ? base.ConnectionString : _dataSource.ConnectionString; + get => DataSource is null ? base.ConnectionString : DataSource.ConnectionString; set { base.ConnectionString = value; - _dataSource = null; + DataSource = null; } } @@ -144,7 +161,7 @@ public override string? ConnectionString { base.DbConnection = value; - _dataSource = null; + DataSource = null; } } @@ -156,12 +173,12 @@ public override string? ConnectionString /// public virtual DbDataSource? DbDataSource { - get => _dataSource; + get => DataSource; set { DbConnection = null; ConnectionString = null; - _dataSource = value; + DataSource = value; } } @@ -185,7 +202,7 @@ public virtual INpgsqlRelationalConnection CreateAdminConnection() Multiplexing = false }.ToString(); - var adminNpgsqlOptions = _dataSource is not null + var adminNpgsqlOptions = DataSource is not null ? npgsqlOptions.WithConnection(((NpgsqlConnection)CreateDbConnection()).CloneWith(adminConnectionString)) : npgsqlOptions.Connection is not null ? npgsqlOptions.WithConnection(DbConnection.CloneWith(adminConnectionString)) @@ -215,9 +232,14 @@ public override Transaction? CurrentAmbientTransaction /// 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 NpgsqlRelationalConnection CloneWith(string connectionString) + public virtual async ValueTask CloneWith( + string connectionString, + bool async, + CancellationToken cancellationToken = default) { - var clonedDbConnection = DbConnection.CloneWith(connectionString); + var clonedDbConnection = async + ? await DbConnection.CloneWithAsync(connectionString, cancellationToken).ConfigureAwait(false) + : DbConnection.CloneWith(connectionString); var relationalOptions = RelationalOptionsExtension.Extract(Dependencies.ContextOptions) .WithConnectionString(null) diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs b/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs index bd6089b70..bbff1a515 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs @@ -19,7 +19,7 @@ static NpgsqlSqlGenerationHelper() using (var conn = new NpgsqlConnection()) { ReservedWords = - new HashSet(conn.GetSchema("ReservedWords").Rows.Cast().Select(r => (string)r["ReservedWord"])); + [..conn.GetSchema("ReservedWords").Rows.Cast().Select(r => (string)r["ReservedWord"])]; } } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index bfd022647..e5719e76e 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.NetworkInformation; @@ -11,7 +12,6 @@ using System.Text.Json; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using Npgsql.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -57,6 +57,7 @@ static NpgsqlTypeMappingSource() /// protected virtual ConcurrentDictionary ClrTypeMappings { get; } + private readonly IReadOnlyList _enumDefinitions; private readonly IReadOnlyList _userRangeDefinitions; private readonly bool _supportsMultiranges; @@ -84,6 +85,7 @@ static NpgsqlTypeMappingSource() private readonly NpgsqlCharacterCharTypeMapping _singleChar = new("character(1)"); private readonly NpgsqlStringTypeMapping _xml = new("xml", NpgsqlDbType.Xml); private readonly NpgsqlStringTypeMapping _citext = new("citext", NpgsqlDbType.Citext); + private readonly NpgsqlStringTypeMapping _jsonpath = new("jsonpath", NpgsqlDbType.JsonPath); // JSON mappings - EF owned entity support private readonly NpgsqlOwnedJsonTypeMapping _jsonbOwned = new("jsonb"); @@ -148,7 +150,7 @@ static NpgsqlTypeMappingSource() private readonly NpgsqlRangeTypeMapping _int8range; private readonly NpgsqlRangeTypeMapping _numrange; private readonly NpgsqlRangeTypeMapping _tsrange; - private readonly NpgsqlRangeTypeMapping _tstzrange; + private readonly NpgsqlRangeTypeMapping _tstzrange, _tstzrangeDto; private readonly NpgsqlRangeTypeMapping _dateOnlyDaterange; private readonly NpgsqlRangeTypeMapping _dateTimeDaterange; @@ -206,6 +208,8 @@ public NpgsqlTypeMappingSource( "tsrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampRange, _timestamp); _tstzrange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( "tstzrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampTzRange, _timestamptz); + _tstzrangeDto = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampTzRange, _timestamptzDto); _dateOnlyDaterange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( "daterange", typeof(NpgsqlRange), NpgsqlDbType.DateRange, _dateDateOnly); _dateTimeDaterange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( @@ -217,75 +221,76 @@ public NpgsqlTypeMappingSource( // https://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "smallint", new RelationalTypeMapping[] { _int2, _int2Byte } }, - { "int2", new RelationalTypeMapping[] { _int2, _int2Byte } }, - { "integer", new[] { _int4 } }, - { "int", new[] { _int4 } }, - { "int4", new[] { _int4 } }, - { "bigint", new[] { _int8 } }, - { "int8", new[] { _int8 } }, - { "real", new[] { _float4 } }, - { "float4", new[] { _float4 } }, - { "double precision", new[] { _float8 } }, - { "float8", new[] { _float8 } }, - { "numeric", new RelationalTypeMapping[] { _numeric, _bigInteger, _numericAsFloat, _numericAsDouble } }, - { "decimal", new RelationalTypeMapping[] { _numeric, _bigInteger, _numericAsFloat, _numericAsDouble } }, - { "money", new[] { _money } }, - { "text", new[] { _text } }, - { "jsonb", new RelationalTypeMapping[] { _jsonbString, _jsonbDocument, _jsonbElement } }, - { "json", new RelationalTypeMapping[] { _jsonString, _jsonDocument, _jsonElement } }, - { "xml", new[] { _xml } }, - { "citext", new[] { _citext } }, - { "character varying", new[] { _varchar } }, - { "varchar", new[] { _varchar } }, + { "smallint", [_int2, _int2Byte] }, + { "int2", [_int2, _int2Byte] }, + { "integer", [_int4] }, + { "int", [_int4] }, + { "int4", [_int4] }, + { "bigint", [_int8] }, + { "int8", [_int8] }, + { "real", [_float4] }, + { "float4", [_float4] }, + { "double precision", [_float8] }, + { "float8", [_float8] }, + { "numeric", [_numeric, _bigInteger, _numericAsFloat, _numericAsDouble] }, + { "decimal", [_numeric, _bigInteger, _numericAsFloat, _numericAsDouble] }, + { "money", [_money] }, + { "text", [_text] }, + { "jsonb", [_jsonbString, _jsonbDocument, _jsonbElement] }, + { "json", [_jsonString, _jsonDocument, _jsonElement] }, + { "jsonpath", [_jsonpath] }, + { "xml", [_xml] }, + { "citext", [_citext] }, + { "character varying", [_varchar] }, + { "varchar", [_varchar] }, // See FindBaseMapping below for special treatment of 'character' - { "timestamp without time zone", new[] { _timestamp } }, - { "timestamp with time zone", new[] { _timestamptz, _timestamptzDto } }, - { "interval", new[] { _interval } }, - { "date", new RelationalTypeMapping[] { _dateDateOnly, _dateDateTime } }, - { "time without time zone", new RelationalTypeMapping[] { _timeTimeOnly, _timeTimeSpan } }, - { "time with time zone", new[] { _timetz } }, - { "boolean", new[] { _bool } }, - { "bool", new[] { _bool } }, - { "bytea", new[] { _bytea } }, - { "uuid", new[] { _uuid } }, - { "bit", new[] { _bit } }, - { "bit varying", new[] { _varbit } }, - { "varbit", new[] { _varbit } }, - { "hstore", new RelationalTypeMapping[] { _hstore, _immutableHstore } }, - { "macaddr", new[] { _macaddr } }, - { "macaddr8", new[] { _macaddr8 } }, - { "inet", new RelationalTypeMapping[] { _inetAsIPAddress, _inetAsNpgsqlInet } }, - { "cidr", new[] { _cidr } }, - { "point", new[] { _point } }, - { "box", new[] { _box } }, - { "line", new[] { _line } }, - { "lseg", new[] { _lseg } }, - { "path", new[] { _path } }, - { "polygon", new[] { _polygon } }, - { "circle", new[] { _circle } }, - { "xid", new[] { _xid } }, - { "xid8", new[] { _xid8 } }, - { "oid", new[] { _oid } }, - { "cid", new[] { _cid } }, - { "regtype", new[] { _regtype } }, - { "lo", new[] { _lo } }, - { "tid", new[] { _tid } }, - { "pg_lsn", new[] { _pgLsn } }, - { "int4range", new[] { _int4range } }, - { "int8range", new[] { _int8range } }, - { "numrange", new[] { _numrange } }, - { "tsrange", new[] { _tsrange } }, - { "tstzrange", new[] { _tstzrange } }, - { "daterange", new[] { _dateOnlyDaterange, _dateTimeDaterange } }, - { "tsquery", new[] { _tsquery } }, - { "tsvector", new[] { _tsvector } }, - { "regconfig", new[] { _regconfig } }, - { "ltree", new[] { _ltree, _ltreeString } }, - { "lquery", new[] { _lquery } }, - { "ltxtquery", new[] { _ltxtquery } }, - { "regdictionary", new[] { _regdictionary } } + { "timestamp without time zone", [_timestamp] }, + { "timestamp with time zone", [_timestamptz, _timestamptzDto] }, + { "interval", [_interval] }, + { "date", [_dateDateOnly, _dateDateTime] }, + { "time without time zone", [_timeTimeOnly, _timeTimeSpan] }, + { "time with time zone", [_timetz] }, + { "boolean", [_bool] }, + { "bool", [_bool] }, + { "bytea", [_bytea] }, + { "uuid", [_uuid] }, + { "bit", [_bit] }, + { "bit varying", [_varbit] }, + { "varbit", [_varbit] }, + { "hstore", [_hstore, _immutableHstore] }, + { "macaddr", [_macaddr] }, + { "macaddr8", [_macaddr8] }, + { "inet", [_inetAsIPAddress, _inetAsNpgsqlInet] }, + { "cidr", [_cidr] }, + { "point", [_point] }, + { "box", [_box] }, + { "line", [_line] }, + { "lseg", [_lseg] }, + { "path", [_path] }, + { "polygon", [_polygon] }, + { "circle", [_circle] }, + { "xid", [_xid] }, + { "xid8", [_xid8] }, + { "oid", [_oid] }, + { "cid", [_cid] }, + { "regtype", [_regtype] }, + { "lo", [_lo] }, + { "tid", [_tid] }, + { "pg_lsn", [_pgLsn] }, + { "int4range", [_int4range] }, + { "int8range", [_int8range] }, + { "numrange", [_numrange] }, + { "tsrange", [_tsrange] }, + { "tstzrange", [_tstzrange, _tstzrangeDto] }, + { "daterange", [_dateOnlyDaterange, _dateTimeDaterange] }, + { "tsquery", [_tsquery] }, + { "tsvector", [_tsvector] }, + { "regconfig", [_regconfig] }, + { "ltree", [_ltree, _ltreeString] }, + { "lquery", [_lquery] }, + { "ltxtquery", [_ltxtquery] }, + { "regdictionary", [_regdictionary] } }; // ReSharper restore CoVariantArrayConversion @@ -348,66 +353,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); - - /// - /// 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] = new RelationalTypeMapping[] { mapping }; - } - } - } - /// /// 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 @@ -418,6 +367,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); @@ -496,11 +446,10 @@ is PropertyInfo globalEnumTypeMappingsProperty } } - // TODO: the following is a workaround/hack for https://github.com/dotnet/efcore/issues/31505 if ((storeTypeName.EndsWith("[]", StringComparison.Ordinal) || storeTypeName is "int4multirange" or "int8multirange" or "nummultirange" or "datemultirange" or "tsmultirange" or "tstzmultirange") - && FindCollectionMapping(mappingInfo, mappingInfo.ClrType!, providerType: null, elementMapping: null) is + && FindCollectionMapping(mappingInfo, mappingInfo.ClrType, providerType: null, elementMapping: null) is RelationalTypeMapping collectionMapping) { return collectionMapping; @@ -515,7 +464,7 @@ is PropertyInfo globalEnumTypeMappingsProperty if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) { // Handle types with the size facet (string, bitarray) - if (mappingInfo.Size is > 0) + if (mappingInfo.Size > 0) { if (clrType == typeof(string)) { @@ -581,7 +530,9 @@ is PropertyInfo globalEnumTypeMappingsProperty /// protected override RelationalTypeMapping? FindCollectionMapping( RelationalTypeMappingInfo info, - Type modelType, + // Note that modelType is nullable (in the relational base signature it isn't) because of array scaffolding, i.e. we call + // FindCollectionMapping from our own FindMapping, where the clrType is null when scaffolding. + Type? modelType, Type? providerType, CoreTypeMapping? elementMapping) { @@ -593,9 +544,6 @@ is PropertyInfo globalEnumTypeMappingsProperty Type concreteCollectionType; Type? elementType = null; - // TODO: modelType can be null (contrary to nullable annotations) only because of https://github.com/dotnet/efcore/issues/31505, - // i.e. we call into here - // If there's a CLR type (i.e. not reverse-engineering), check that it's a compatible enumerable. if (modelType is not null) { // We do GetElementType for multidimensional arrays - these don't implement generic IEnumerable<> @@ -814,6 +762,70 @@ 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; + string? schema; + string name; + + 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); + + if (enumDefinition is null) + { + return null; + } + + (name, schema) = (enumDefinition.StoreTypeName, enumDefinition.StoreTypeSchema); + } + else + { + // If the user is specifying the store type manually, they are not expected to have quotes in the name (e.g. because of upper- + // case characters). + // However, if we infer an enum array type mapping from an element (e.g. someEnums.Contains(b.SomeEnumColumn)), we get the + // element's store type - which for enums is quoted - and add []; so we get e.g. "MyEnum"[]. So we need to support quoted + // names here, by parsing the name and stripping the quotes. + ParseStoreTypeName(storeType, out name, out schema, out var size, out var precision, out var scale); + + enumDefinition = schema is null + ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name) + : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name && m.StoreTypeSchema == schema); + + 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). + 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 @@ -836,7 +848,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) { @@ -873,16 +885,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); @@ -915,6 +927,26 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) return rangeStoreType is null ? null : FindMapping(containerClrType, rangeStoreType); } + // If this containment of a range within a multirange, just flow down to the general collection mapping logic; multiranges are + // handled just like normal collections over ranges (since they can be unnested). + // However, we also support containment of the base type (e.g. int) directly in its multirange (e.g. int4range). A multirange + // is *not* a collection over the base type, so handle that specific case here. + if (containerClrType.IsMultirange() && !containeeTypeMapping.ClrType.IsRange()) + { + var multirangeStoreType = containeeTypeMapping.StoreType switch + { + "int" or "integer" => "int4multirange", + "bigint" => "int8multirange", + "decimal" or "numeric" => "nummultirange", + "date" => "datemultirange", + "timestamp" or "timestamp without time zone" => "tsmultirange", + "timestamptz" or "timestamp with time zone" => "tstzmultirange", + _ => null + }; + + return !_supportsMultiranges || multirangeStoreType is null ? null : FindMapping(containerClrType, multirangeStoreType); + } + // Then, try to find the mapping with the containee mapping as the element type mapping. // This is the standard EF lookup mechanism, and takes care of regular arrays and multiranges, which are supported as full primitive // collections. @@ -953,6 +985,8 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) ref int? precision, ref int? scale) { + // TODO: Reimplement over ParseStoreTypeName below + if (storeTypeName is null) { return null; @@ -1037,4 +1071,155 @@ private static bool NameBasesUsesPrecision(ReadOnlySpan span) return new StringBuilder(preParens.Length).Append(preParens).Append(postParens).ToString(); } + + internal static void ParseStoreTypeName( + string storeTypeName, + out string name, + out string? schema, + out int? size, + out int? precision, + out int? scale) + { + var s = storeTypeName.AsSpan().Trim(); + var i = 0; + size = precision = scale = null; + + if (s.EndsWith("[]", StringComparison.Ordinal)) + { + // If this is an array store type, any facets (size, precision...) apply to the element and not to the array (e.g. varchar(32)[] + // is an array mapping with Size=null over an element mapping of varchar with Size=32). So just add everything up to the end. + // Note that if there's a schema (e.g. foo.varchar(32)[]), we return name=varchar(32), schema=foo. + name = s.ToString(); + schema = null; + return; + } + + name = ParseNameComponent(s); + + if (i < s.Length && s[i] == '.') + { + i++; + schema = name; + name = ParseNameComponent(s); + } + else + { + schema = null; + } + + s = s[i..]; + + if (s.Length == 0 || s[0] != '(') + { + // No facets + return; + } + + s = s[1..]; + + var closeParen = s.IndexOf(")", StringComparison.Ordinal); + if (closeParen == -1) + { + return; + } + + var inParens = s[..closeParen].Trim(); + // There may be stuff after the closing parentheses (e.g. timestamp(3) with time zone) + var postParens = s.Slice(closeParen + 1); + + switch (s.IndexOf(",", StringComparison.Ordinal)) + { + // No comma inside the parentheses, parse the value either as size or precision + case -1: + if (!int.TryParse(inParens, out var p)) + { + return; + } + + if (NameBasesUsesPrecision(name)) + { + precision = p; + // scale = 0; + } + else + { + size = p; + } + + break; + + case var comma: + if (int.TryParse(s[..comma].Trim(), out var parsedPrecision)) + { + precision = parsedPrecision; + } + else + { + return; + } + + if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale)) + { + scale = parsedScale; + } + else + { + return; + } + + break; + } + + if (postParens.Length > 0) + { + // There's stuff after the parentheses (e.g. time(3) with time zone), append to the name + name += postParens.ToString(); + } + + string ParseNameComponent(ReadOnlySpan s) + { + var inQuotes = false; + StringBuilder builder = new(); + + if (s[i] == '"') + { + inQuotes = true; + i++; + } + + var start = i; + + for (; i < s.Length; i++) + { + var c = s[i]; + + if (inQuotes) + { + if (c == '"') + { + if (i + 1 < s.Length && s[i + 1] == '"') + { + builder.Append('"'); + i++; + continue; + } + + i++; + break; + } + } + else if (!char.IsWhiteSpace(c) && !char.IsAsciiLetterOrDigit(c) && c != '_') + { + break; + } + + builder.Append(c); + } + + var length = i - start; + return length == storeTypeName.Length + ? storeTypeName + : builder.ToString(); + } + } } diff --git a/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs b/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs index 0cbfea720..982d58eaa 100644 --- a/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs +++ b/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs @@ -105,95 +105,185 @@ private static Expression> ArrayConversionExpression(); - var variables = new List(4) - { - output, - lengthVariable, - loopVariable - }; + var variables = new List { output, lengthVariable }; Expression getInputLength; - Func indexer; + Func? indexer; - if (typeof(TInput).IsArray) - { - getInputLength = ArrayLength(input); - indexer = i => ArrayAccess(input, i); - } - else if (typeof(TInput).IsGenericType - && typeof(TInput).GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>))) - { - getInputLength = Property( - input, - typeof(TInput).GetProperty("Count") - // If TInput is an interface (IList), its Count property needs to be found on ICollection - ?? typeof(ICollection<>).MakeGenericType(typeof(TInput).GetGenericArguments()[0]).GetProperty("Count")!); - indexer = i => Property(input, input.Type.FindIndexerProperty()!, i); - } - else + // The conversion is going to depend on what kind of input we have: array, list, collection, or arbitrary IEnumerable. + // For array/list we can get the length and index inside, so we can do an efficient for loop. + // For other ICollections (e.g. HashSet) we can get the length (and so pre-allocate the output), but we can't index; so we + // get an enumerator and use that. + // For arbitrary IEnumerable, we can't get the length so we can't preallocate output arrays; so we to call ToList() on it and then + // process that (note that we could avoid that when the output is a List rather than an array). + var inputInterfaces = input.Type.GetInterfaces(); + switch (input.Type) { - // Input collection isn't typed as an ICollection; it can be *typed* as an IEnumerable, but we only support concrete - // instances being ICollection. Emit code that casts the type at runtime. - var iListType = typeof(IList<>).MakeGenericType(typeof(TInput).GetGenericArguments()[0]); + // Input is typed as an array - we can get its length and index into it + case { IsArray: true }: + getInputLength = ArrayLength(input); + indexer = i => ArrayAccess(input, i); + break; - var convertedInput = Variable(iListType, "convertedInput"); - variables.Add(convertedInput); + // Input is typed as an IList - we can get its length and index into it + case { IsGenericType: true } when inputInterfaces.Append(input.Type) + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>)): + { + getInputLength = Property( + input, + input.Type.GetProperty("Count") + // If TInput is an interface (IList), its Count property needs to be found on ICollection + ?? typeof(ICollection<>).MakeGenericType(input.Type.GetGenericArguments()[0]).GetProperty("Count")!); + indexer = i => Property(input, input.Type.FindIndexerProperty()!, i); + break; + } - expressions.Add(Assign(convertedInput, Convert(input, convertedInput.Type))); + // Input is typed as an ICollection - we can get its length, but we can't index into it + case { IsGenericType: true } when inputInterfaces.Append(input.Type) + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)): + { + getInputLength = Property( + input, typeof(ICollection<>).MakeGenericType(input.Type.GetGenericArguments()[0]).GetProperty("Count")!); + indexer = null; + break; + } - // TODO: Check and properly throw for non-IList, e.g. set - getInputLength = Property( - convertedInput, typeof(ICollection<>).MakeGenericType(typeof(TInput).GetGenericArguments()[0]).GetProperty("Count")!); - indexer = i => Property(convertedInput, iListType.FindIndexerProperty()!, i); + // Input is typed as an IEnumerable - we can't get its length, and we can't index into it. + // All we can do is call ToList() on it and then process that. + case { IsGenericType: true } when inputInterfaces.Append(input.Type) + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)): + { + // TODO: In theory, we could add runtime checks for array/list/collection, downcast for those cases and include + // the logic from the other switch cases here. + convertedInput = Variable(typeof(List<>).MakeGenericType(inputElementType), "convertedInput"); + variables.Add(convertedInput); + expressions.Add( + Assign( + convertedInput, + Call(typeof(Enumerable).GetMethod(nameof(Enumerable.ToList))!.MakeGenericMethod(inputElementType), input))); + getInputLength = Property(convertedInput, convertedInput.Type.GetProperty("Count")!); + indexer = i => Property(convertedInput, convertedInput.Type.FindIndexerProperty()!, i); + break; + } + + default: + throw new NotSupportedException($"Array value converter input type must be an IEnumerable, but is {typeof(TInput)}"); } expressions.AddRange( - new[] - { - // Get the length of the input array or list - // var length = input.Length; - Assign(lengthVariable, getInputLength), - - // Allocate an output array or list - // var result = new int[length]; - Assign( - output, typeof(TConcreteOutput).IsArray - ? NewArrayBounds(outputElementType, lengthVariable) - : typeof(TConcreteOutput).GetConstructor(new[] { typeof(int) }) is ConstructorInfo ctorWithLength - ? New(ctorWithLength, lengthVariable) - : New(typeof(TConcreteOutput).GetConstructor(Array.Empty())!)), - - // Loop over the elements, applying the element converter on them one by one - // for (var i = 0; i < length; i++) - // { - // result[i] = input[i]; - // } + [ + // Get the length of the input array or list + // var length = input.Length; + Assign(lengthVariable, getInputLength), + + // Allocate an output array or list + // var result = new int[length]; + Assign( + output, typeof(TConcreteOutput).IsArray + ? NewArrayBounds(outputElementType, lengthVariable) + : typeof(TConcreteOutput).GetConstructor([typeof(int)]) is ConstructorInfo ctorWithLength + ? New(ctorWithLength, lengthVariable) + : New(typeof(TConcreteOutput).GetConstructor([])!)) + ]); + + if (indexer is not null) + { + // Good case: the input is an array or list, so we can index into it. Generate code for an efficient for loop, which applies + // the element converter on each element. + // for (var i = 0; i < length; i++) + // { + // result[i] = input[i]; + // } + var counter = Parameter(typeof(int), "i"); + + expressions.Add( ForLoop( - loopVar: loopVariable, + loopVar: counter, initValue: Constant(0), - condition: LessThan(loopVariable, lengthVariable), - increment: AddAssign(loopVariable, Constant(1)), + condition: LessThan(counter, lengthVariable), + increment: AddAssign(counter, Constant(1)), loopContent: typeof(TConcreteOutput).IsArray ? Assign( - ArrayAccess(output, loopVariable), + ArrayAccess(output, counter), elementConversionExpression is null - ? indexer(loopVariable) - : Invoke(elementConversionExpression, indexer(loopVariable))) + ? indexer(counter) + : Invoke(elementConversionExpression, indexer(counter))) : Call( output, - typeof(TConcreteOutput).GetMethod("Add", new[] { outputElementType })!, + typeof(TConcreteOutput).GetMethod("Add", [outputElementType])!, elementConversionExpression is null - ? indexer(loopVariable) - : Invoke(elementConversionExpression, indexer(loopVariable)))), - output - }); + ? indexer(counter) + : Invoke(elementConversionExpression, indexer(counter))))); + } + else + { + // Bad case: the input is not an array or list, but is a collection (e.g. HashSet), so we can't index into it. + // Generate code for a less efficient enumerator-based iteration. + // enumerator = input.GetEnumerator(); + // counter = 0; + // while (enumerator.MoveNext()) + // { + // output[counter] = enumerator.Current; + // counter++; + // } + var enumerableType = typeof(IEnumerable<>).MakeGenericType(inputElementType); + var enumeratorType = typeof(IEnumerator<>).MakeGenericType(inputElementType); + + var enumeratorVariable = Variable(enumeratorType, "enumerator"); + var counterVariable = Variable(typeof(int), "variable"); + variables.AddRange([enumeratorVariable, counterVariable]); + + expressions.AddRange( + [ + // enumerator = input.GetEnumerator(); + Assign(enumeratorVariable, Call(input, enumerableType.GetMethod(nameof(IEnumerable.GetEnumerator))!)), + + // counter = 0; + Assign(counterVariable, Constant(0)) + ]); + + var breakLabel = Label("LoopBreak"); + + var loop = + Loop( + IfThenElse( + Equal(Call(enumeratorVariable, typeof(IEnumerator).GetMethod(nameof(IEnumerator.MoveNext))!), Constant(true)), + Block( + typeof(TConcreteOutput).IsArray + // output[counter] = enumerator.Current; + ? Assign( + ArrayAccess(output, counterVariable), + elementConversionExpression is null + ? Property(enumeratorVariable, "Current") + : Invoke(elementConversionExpression, Property(enumeratorVariable, "Current"))) + // output.Add(enumerator.Current); + : Call( + output, + typeof(TConcreteOutput).GetMethod("Add", [outputElementType])!, + elementConversionExpression is null + ? Property(enumeratorVariable, "Current") + : Invoke(elementConversionExpression, Property(enumeratorVariable, "Current"))), + + // counter++; + AddAssign(counterVariable, Constant(1))), + Break(breakLabel)), + breakLabel); + + expressions.Add( + TryFinally( + loop, + Call(enumeratorVariable, typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose))!))); + } + + // return output; + expressions.Add(output); return Lambda>( // First, check if the given array value is null and return null immediately if so @@ -214,7 +304,7 @@ private static Expression ForLoop( var initAssign = Assign(loopVar, initValue); var breakLabel = Label("LoopBreak"); var loop = Block( - new[] { loopVar }, + [loopVar], initAssign, Loop( IfThenElse( diff --git a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs index 09778a6bf..2bc592ff9 100644 --- a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs +++ b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs @@ -189,6 +189,16 @@ await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, } catch (Exception ex) when (ex is not DbUpdateException and not OperationCanceledException) { + // If the commandIndex points after the last command, attribute the error to the last command. + // This can happen when an AFTER INSERT trigger raises an exception - the insertion itself is successful, and the error comes + // afterwards, as if belonging to the next command. When there's indeed a next command, there's no way to know whether the + // error indeed belongs to it or comes from a trigger on the previous (we assume the former), but when we're the last command, + // at least avoid indexing beyond the end of the array. See #3007. + if (commandIndex == ModificationCommands.Count) + { + commandIndex--; + } + throw new DbUpdateException( RelationalStrings.UpdateStoreException, ex, diff --git a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs index aea8747aa..fa7d1fad0 100644 --- a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs +++ b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs @@ -225,7 +225,7 @@ public override ResultSetMapping AppendDeleteOperation( requiresTransaction = false; - AppendDeleteCommand(commandStringBuilder, name, schema, Array.Empty(), conditionOperations); + AppendDeleteCommand(commandStringBuilder, name, schema, [], conditionOperations); return ResultSetMapping.NoResults; } diff --git a/src/EFCore.PG/Utilities/Util.cs b/src/EFCore.PG/Utilities/Util.cs index 9f0c14ae4..97bca187c 100644 --- a/src/EFCore.PG/Utilities/Util.cs +++ b/src/EFCore.PG/Utilities/Util.cs @@ -3,17 +3,22 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; internal static class Statics { internal static readonly bool[][] TrueArrays = - { - Array.Empty(), - new[] { true }, - new[] { true, true }, - new[] { true, true, true }, - new[] { true, true, true, true }, - new[] { true, true, true, true, true }, - new[] { true, true, true, true, true, true }, - new[] { true, true, true, true, true, true, true }, - new[] { true, true, true, true, true, true, true, true } - }; + [ + [], + [true], + [true, true], + [true, true, true], + [true, true, true, true], + [true, true, true, true, true], + [true, true, true, true, true, true], + [true, true, true, true, true, true, true], + [true, true, true, true, true, true, true, true] + ]; - internal static readonly bool[][] FalseArrays = { Array.Empty(), new[] { false }, new[] { false, false } }; + internal static readonly bool[][] FalseArrays = [ + [], + [false], + [false, false], + [false, false, false] + ]; } diff --git a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs b/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs index 4663f098b..51d91b739 100644 --- a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs +++ b/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs @@ -12,8 +12,9 @@ public interface INpgsqlSequenceValueGeneratorFactory /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - ValueGenerator Create( + ValueGenerator? TryCreate( IProperty property, + Type clrType, NpgsqlSequenceValueGeneratorState generatorState, INpgsqlRelationalConnection connection, IRawSqlCommandBuilder rawSqlCommandBuilder, diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs index 936cf3a06..37bf81d93 100644 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs +++ b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs @@ -26,15 +26,14 @@ public NpgsqlSequenceValueGeneratorFactory( /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public virtual ValueGenerator Create( + public virtual ValueGenerator? TryCreate( IProperty property, + Type type, NpgsqlSequenceValueGeneratorState generatorState, INpgsqlRelationalConnection connection, IRawSqlCommandBuilder rawSqlCommandBuilder, IRelationalCommandDiagnosticsLogger commandLogger) { - var type = property.ClrType.UnwrapNullableType().UnwrapEnumType(); - if (type == typeof(long)) { return new NpgsqlSequenceHiLoValueGenerator( @@ -89,8 +88,6 @@ public virtual ValueGenerator Create( rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); } - throw new ArgumentException( - CoreStrings.InvalidValueGeneratorFactoryProperty( - nameof(NpgsqlSequenceValueGeneratorFactory), property.Name, property.DeclaringType.DisplayName())); + return null; } } diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs index f18313457..808532b42 100644 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs +++ b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs @@ -49,16 +49,50 @@ public NpgsqlValueGeneratorSelector( /// 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 override ValueGenerator Select(IProperty property, ITypeBase typeBase) - => property.GetValueGeneratorFactory() is null - && property.GetValueGenerationStrategy() == NpgsqlValueGenerationStrategy.SequenceHiLo - ? _sequenceFactory.Create( - property, - Cache.GetOrAddSequenceState(property, _connection), - _connection, - _rawSqlCommandBuilder, - _commandLogger) - : base.Select(property, typeBase); + public override bool TrySelect(IProperty property, ITypeBase typeBase, out ValueGenerator? valueGenerator) + { + if (property.GetValueGeneratorFactory() != null + || property.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.SequenceHiLo) + { + return base.TrySelect(property, typeBase, out valueGenerator); + } + + var propertyType = property.ClrType.UnwrapNullableType().UnwrapEnumType(); + + valueGenerator = _sequenceFactory.TryCreate( + property, + propertyType, + Cache.GetOrAddSequenceState(property, _connection), + _connection, + _rawSqlCommandBuilder, + _commandLogger); + + if (valueGenerator != null) + { + return true; + } + + var converter = property.GetTypeMapping().Converter; + if (converter != null + && converter.ProviderClrType != propertyType) + { + valueGenerator = _sequenceFactory.TryCreate( + property, + converter.ProviderClrType, + Cache.GetOrAddSequenceState(property, _connection), + _connection, + _rawSqlCommandBuilder, + _commandLogger); + + if (valueGenerator != null) + { + valueGenerator = valueGenerator.WithConverter(converter); + return true; + } + } + + return false; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -67,9 +101,14 @@ public override ValueGenerator Select(IProperty property, ITypeBase typeBase) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override ValueGenerator? FindForType(IProperty property, ITypeBase typeBase, Type clrType) - => property.ClrType.UnwrapNullableType() == typeof(Guid) - ? property.ValueGenerated == ValueGenerated.Never || property.GetDefaultValueSql() is not null - ? new TemporaryGuidValueGenerator() - : new GuidValueGenerator() - : base.FindForType(property, typeBase, clrType); + => property.ClrType.UnwrapNullableType() switch + { + var t when t == typeof(Guid) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new NpgsqlSequentialGuidValueGenerator(), + + var t when t == typeof(string) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new NpgsqlSequentialStringValueGenerator(), + + _ => base.FindForType(property, typeBase, clrType) + }; } diff --git a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialGuidValueGenerator.cs b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialGuidValueGenerator.cs new file mode 100644 index 000000000..69d90f4f1 --- /dev/null +++ b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialGuidValueGenerator.cs @@ -0,0 +1,107 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration; + +/// +/// This API supports the Entity Framework Core infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public class NpgsqlSequentialGuidValueGenerator : ValueGenerator +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool GeneratesTemporaryValues => false; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Guid Next(EntityEntry entry) => BorrowedFromNet9.CreateVersion7(timestamp: DateTimeOffset.UtcNow); + + // Code borrowed from .NET 9 should be removed as soon as the target framework includes such code + #region Borrowed from .NET 9 + +#pragma warning disable IDE0007 // Use implicit type -- Avoid changes to code borrowed from BCL + + // https://github.com/dotnet/runtime/blob/f402418aaed508c1d77e41b942e3978675183bfc/src/libraries/System.Private.CoreLib/src/System/Guid.cs + internal static class BorrowedFromNet9 + { + private const byte Variant10xxMask = 0xC0; + private const byte Variant10xxValue = 0x80; + + private const ushort VersionMask = 0xF000; + private const ushort Version7Value = 0x7000; + + /// Creates a new according to RFC 9562, following the Version 7 format. + /// A new according to RFC 9562, following the Version 7 format. + /// + /// This uses to determine the Unix Epoch timestamp source. + /// This seeds the rand_a and rand_b sub-fields with random data. + /// + public static Guid CreateVersion7() => CreateVersion7(DateTimeOffset.UtcNow); + + /// Creates a new according to RFC 9562, following the Version 7 format. + /// The date time offset used to determine the Unix Epoch timestamp. + /// A new according to RFC 9562, following the Version 7 format. + /// represents an offset prior to . + /// + /// This seeds the rand_a and rand_b sub-fields with random data. + /// + public static Guid CreateVersion7(DateTimeOffset timestamp) + { + // NewGuid uses CoCreateGuid on Windows and Interop.GetCryptographicallySecureRandomBytes on Unix to get + // cryptographically-secure random bytes. We could use Interop.BCrypt.BCryptGenRandom to generate the random + // bytes on Windows, as is done in RandomNumberGenerator, but that's measurably slower than using CoCreateGuid. + // And while CoCreateGuid only generates 122 bits of randomness, the other 6 bits being for the version / variant + // fields, this method also needs those bits to be non-random, so we can just use NewGuid for efficiency. + var result = Guid.NewGuid(); + + // 2^48 is roughly 8925.5 years, which from the Unix Epoch means we won't + // overflow until around July of 10,895. So there isn't any need to handle + // it given that DateTimeOffset.MaxValue is December 31, 9999. However, we + // can't represent timestamps prior to the Unix Epoch since UUIDv7 explicitly + // stores a 48-bit unsigned value, so we do need to throw if one is passed in. + + var unix_ts_ms = timestamp.ToUnixTimeMilliseconds(); + ArgumentOutOfRangeException.ThrowIfNegative(unix_ts_ms, nameof(timestamp)); + + ref var resultClone = ref Unsafe.As(ref result); // Deviation from BLC: Reinterpret Guid as our own type so that we can manipulate its private fields + + Unsafe.AsRef(in resultClone._a) = (int)(unix_ts_ms >> 16); + Unsafe.AsRef(in resultClone._b) = (short)unix_ts_ms; + + Unsafe.AsRef(in resultClone._c) = (short)(resultClone._c & ~VersionMask | Version7Value); + Unsafe.AsRef(in resultClone._d) = (byte)(resultClone._d & ~Variant10xxMask | Variant10xxValue); + + return result; + } + } + + /// + /// Used to manipulate the private fields of a like its internal methods do, by treating a as a . + /// + [StructLayout(LayoutKind.Sequential)] + internal readonly struct GuidDoppleganger + { +#pragma warning disable IDE1006 // Naming Styles -- Avoid further changes to code borrowed from BCL when working with the current type + internal readonly int _a; // Do not rename (binary serialization) + internal readonly short _b; // Do not rename (binary serialization) + internal readonly short _c; // Do not rename (binary serialization) + internal readonly byte _d; // Do not rename (binary serialization) + internal readonly byte _e; // Do not rename (binary serialization) + internal readonly byte _f; // Do not rename (binary serialization) + internal readonly byte _g; // Do not rename (binary serialization) + internal readonly byte _h; // Do not rename (binary serialization) + internal readonly byte _i; // Do not rename (binary serialization) + internal readonly byte _j; // Do not rename (binary serialization) + internal readonly byte _k; // Do not rename (binary serialization) +#pragma warning restore IDE1006 // Naming Styles + } + +#pragma warning restore IDE0007 // Use implicit type + + #endregion +} diff --git a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs new file mode 100644 index 000000000..8c4f595dc --- /dev/null +++ b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs @@ -0,0 +1,22 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration; + +/// +/// This API supports the Entity Framework Core infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public class NpgsqlSequentialStringValueGenerator : ValueGenerator +{ + private readonly NpgsqlSequentialGuidValueGenerator _guidGenerator = new(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool GeneratesTemporaryValues => false; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override string Next(EntityEntry entry) => _guidGenerator.Next(entry).ToString(); +} diff --git a/src/Shared/CodeAnnotations.cs b/src/Shared/CodeAnnotations.cs index 417866a62..32ec3e251 100644 --- a/src/Shared/CodeAnnotations.cs +++ b/src/Shared/CodeAnnotations.cs @@ -91,14 +91,9 @@ public StringFormatMethodAttribute(string formatParameterName) [AttributeUsage( AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)] -internal sealed class ValueProviderAttribute : Attribute +internal sealed class ValueProviderAttribute(string name) : Attribute { - public ValueProviderAttribute(string name) - { - Name = name; - } - - public string Name { get; } + public string Name { get; } = name; } /// @@ -164,9 +159,7 @@ public ValueRangeAttribute(ulong value) | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Delegate)] -internal sealed class NonNegativeValueAttribute : Attribute -{ -} +internal sealed class NonNegativeValueAttribute : Attribute; /// /// Indicates that the function argument should be a string literal and match one @@ -180,9 +173,7 @@ internal sealed class NonNegativeValueAttribute : Attribute /// } /// [AttributeUsage(AttributeTargets.Parameter)] -internal sealed class InvokerParameterNameAttribute : Attribute -{ -} +internal sealed class InvokerParameterNameAttribute : Attribute; /// /// Describes dependency between method input and output. @@ -229,22 +220,16 @@ internal sealed class InvokerParameterNameAttribute : Attribute /// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -internal sealed class ContractAnnotationAttribute : Attribute +internal sealed class ContractAnnotationAttribute(string contract, bool forceFullStates) : Attribute { public ContractAnnotationAttribute(string contract) : this(contract, false) { } - public ContractAnnotationAttribute(string contract, bool forceFullStates) - { - Contract = contract; - ForceFullStates = forceFullStates; - } + public string Contract { get; } = contract; - public string Contract { get; } - - public bool ForceFullStates { get; } + public bool ForceFullStates { get; } = forceFullStates; } /// @@ -257,19 +242,14 @@ public ContractAnnotationAttribute(string contract, bool forceFullStates) /// } /// [AttributeUsage(AttributeTargets.All)] -internal sealed class LocalizationRequiredAttribute : Attribute +internal sealed class LocalizationRequiredAttribute(bool required) : Attribute { public LocalizationRequiredAttribute() : this(true) { } - public LocalizationRequiredAttribute(bool required) - { - Required = required; - } - - public bool Required { get; } + public bool Required { get; } = required; } /// @@ -293,9 +273,7 @@ public LocalizationRequiredAttribute(bool required) /// } /// [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] -internal sealed class CannotApplyEqualityOperatorAttribute : Attribute -{ -} +internal sealed class CannotApplyEqualityOperatorAttribute : Attribute; /// /// When applied to a target attribute, specifies a requirement for any type marked @@ -310,14 +288,9 @@ internal sealed class CannotApplyEqualityOperatorAttribute : Attribute /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] [BaseTypeRequired(typeof(Attribute))] -internal sealed class BaseTypeRequiredAttribute : Attribute +internal sealed class BaseTypeRequiredAttribute(Type baseType) : Attribute { - public BaseTypeRequiredAttribute(Type baseType) - { - BaseType = baseType; - } - - public Type BaseType { get; } + public Type BaseType { get; } = baseType; } /// @@ -325,7 +298,8 @@ public BaseTypeRequiredAttribute(Type baseType) /// so this symbol will not be reported as unused (as well as by other usage inspections). /// [AttributeUsage(AttributeTargets.All)] -internal sealed class UsedImplicitlyAttribute : Attribute +internal sealed class UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + : Attribute { public UsedImplicitlyAttribute() : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) @@ -342,15 +316,9 @@ public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) { } - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - public ImplicitUseKindFlags UseKindFlags { get; } + public ImplicitUseKindFlags UseKindFlags { get; } = useKindFlags; - public ImplicitUseTargetFlags TargetFlags { get; } + public ImplicitUseTargetFlags TargetFlags { get; } = targetFlags; } /// @@ -360,7 +328,8 @@ public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTar /// is used implicitly. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] -internal sealed class MeansImplicitUseAttribute : Attribute +internal sealed class MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + : Attribute { public MeansImplicitUseAttribute() : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) @@ -377,15 +346,9 @@ public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) { } - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } = useKindFlags; - [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } = targetFlags; } /// @@ -450,14 +413,10 @@ internal enum ImplicitUseTargetFlags /// } /// [AttributeUsage(AttributeTargets.Parameter)] -internal sealed class NoEnumerationAttribute : Attribute -{ -} +internal sealed class NoEnumerationAttribute : Attribute; /// /// Indicates that the marked parameter is a regular expression pattern. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] -internal sealed class RegexPatternAttribute : Attribute -{ -} \ No newline at end of file +internal sealed class RegexPatternAttribute : Attribute; \ No newline at end of file diff --git a/src/Shared/NonCapturingLazyInitializer.cs b/src/Shared/NonCapturingLazyInitializer.cs index a8d623031..ece9b1944 100644 --- a/src/Shared/NonCapturingLazyInitializer.cs +++ b/src/Shared/NonCapturingLazyInitializer.cs @@ -1,7 +1,5 @@ using System.Diagnostics.CodeAnalysis; -#nullable enable - namespace Microsoft.EntityFrameworkCore.Internal; internal static class NonCapturingLazyInitializer diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index d37c585c3..6965d22cb 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -1,8 +1,6 @@ using System.Runtime.CompilerServices; using System.Text; -#nullable enable - // ReSharper disable once CheckNamespace namespace System; @@ -303,7 +301,7 @@ public static IEnumerable GetTypesInHierarchy(this Type type) public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[] types) { - types ??= Array.Empty(); + types ??= []; return type.GetTypeInfo().DeclaredConstructors .SingleOrDefault( @@ -540,7 +538,7 @@ public static IEnumerable GetNamespaces(this Type type) public static ConstantExpression GetDefaultValueConstant(this Type type) => (ConstantExpression)_generateDefaultValueConstantMethod - .MakeGenericMethod(type).Invoke(null, Array.Empty()); + .MakeGenericMethod(type).Invoke(null, []); private static readonly MethodInfo _generateDefaultValueConstantMethod = typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); diff --git a/test/Directory.Build.props b/test/Directory.Build.props index e4c47baca..8648a51f4 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,7 +3,7 @@ - net8.0 + net9.0 false false diff --git a/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs new file mode 100644 index 000000000..dcf301eba --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace Npgsql.EntityFrameworkCore.PostgreSQL; + +public class BadDataJsonDeserializationSqlServerTest : BadDataJsonDeserializationTestBase +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseNpgsql(b => b.UseNetTopologySuite())); +} diff --git a/test/EFCore.PG.FunctionalTests/BatchingTest.cs b/test/EFCore.PG.FunctionalTests/BatchingTest.cs index 2ae6bafb5..f7c87c74d 100644 --- a/test/EFCore.PG.FunctionalTests/BatchingTest.cs +++ b/test/EFCore.PG.FunctionalTests/BatchingTest.cs @@ -7,14 +7,9 @@ // ReSharper disable InconsistentNaming namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class BatchingTest : IClassFixture +public class BatchingTest(BatchingTest.BatchingTestFixture fixture) : IClassFixture { - public BatchingTest(BatchingTestFixture fixture) - { - Fixture = fixture; - } - - protected BatchingTestFixture Fixture { get; } + protected BatchingTestFixture Fixture { get; } = fixture; [Theory] [InlineData(true, true, true)] @@ -25,11 +20,11 @@ public BatchingTest(BatchingTestFixture fixture) [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(false, false, false)] - public void Inserts_are_batched_correctly(bool clientPk, bool clientFk, bool clientOrder) + public async Task Inserts_are_batched_correctly(bool clientPk, bool clientFk, bool clientOrder) { var expectedBlogs = new List(); - ExecuteWithStrategyInTransaction( - context => + await ExecuteWithStrategyInTransactionAsync( + async context => { var owner1 = new Owner(); var owner2 = new Owner(); @@ -58,18 +53,18 @@ public void Inserts_are_batched_correctly(bool clientPk, bool clientFk, bool cli expectedBlogs.Add(blog); } - context.SaveChanges(); + await context.SaveChangesAsync(); }, context => AssertDatabaseState(context, clientOrder, expectedBlogs)); } [Fact] - public void Inserts_and_updates_are_batched_correctly() + public async Task Inserts_and_updates_are_batched_correctly() { var expectedBlogs = new List(); - ExecuteWithStrategyInTransaction( - context => + await ExecuteWithStrategyInTransactionAsync( + async context => { var owner1 = new Owner { Name = "0" }; var owner2 = new Owner { Name = "1" }; @@ -111,35 +106,35 @@ public void Inserts_and_updates_are_batched_correctly() context.Set().Add(blog3); expectedBlogs.Add(blog3); - context.SaveChanges(); + await context.SaveChangesAsync(); }, context => AssertDatabaseState(context, true, expectedBlogs)); } [Fact] - public void Inserts_when_database_type_is_different() - => ExecuteWithStrategyInTransaction( - context => + public Task Inserts_when_database_type_is_different() + => ExecuteWithStrategyInTransactionAsync( + async context => { var owner1 = new Owner { Id = "0", Name = "Zero" }; var owner2 = new Owner { Id = "A", Name = string.Join("", Enumerable.Repeat('A', 900)) }; context.Owners.Add(owner1); context.Owners.Add(owner2); - context.SaveChanges(); + await context.SaveChangesAsync(); }, - context => Assert.Equal(2, context.Owners.Count())); + async context => Assert.Equal(2, await context.Owners.CountAsync())); [ConditionalTheory] [InlineData(3)] [InlineData(4)] - public void Inserts_are_batched_only_when_necessary(int minBatchSize) + public Task Inserts_are_batched_only_when_necessary(int minBatchSize) { var expectedBlogs = new List(); - TestHelpers.ExecuteWithStrategyInTransaction( + return TestHelpers.ExecuteWithStrategyInTransactionAsync( () => (BloggingContext)Fixture.CreateContext(minBatchSize), UseTransaction, - context => + async context => { var owner = new Owner(); context.Owners.Add(owner); @@ -154,7 +149,7 @@ public void Inserts_are_batched_only_when_necessary(int minBatchSize) Fixture.TestSqlLoggerFactory.Clear(); - context.SaveChanges(); + await context.SaveChangesAsync(); Assert.Contains( minBatchSize == 3 @@ -168,13 +163,13 @@ public void Inserts_are_batched_only_when_necessary(int minBatchSize) }, context => AssertDatabaseState(context, false, expectedBlogs)); } - private void AssertDatabaseState(DbContext context, bool clientOrder, List expectedBlogs) + private async Task AssertDatabaseState(DbContext context, bool clientOrder, List expectedBlogs) { expectedBlogs = clientOrder ? expectedBlogs.OrderBy(b => b.Order).ToList() : expectedBlogs.OrderBy(b => b.Id).ToList(); var actualBlogs = clientOrder - ? context.Set().OrderBy(b => b.Order).ToList() + ? await context.Set().OrderBy(b => b.Order).ToListAsync() : expectedBlogs.OrderBy(b => b.Id).ToList(); Assert.Equal(expectedBlogs.Count, actualBlogs.Count); @@ -192,22 +187,17 @@ private void AssertDatabaseState(DbContext context, bool clientOrder, List private BloggingContext CreateContext() => (BloggingContext)Fixture.CreateContext(); - private void ExecuteWithStrategyInTransaction( - Action testOperation, - Action nestedTestOperation) - => TestHelpers.ExecuteWithStrategyInTransaction( + private Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func nestedTestOperation) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( CreateContext, UseTransaction, testOperation, nestedTestOperation); protected void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); - private class BloggingContext : PoolableDbContext + private class BloggingContext(DbContextOptions options) : PoolableDbContext(options) { - public BloggingContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity( @@ -264,8 +254,8 @@ protected override ITestStoreFactory TestStoreFactory protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Update.Name; - protected override void Seed(PoolableDbContext context) - => context.Database.EnsureCreatedResiliently(); + protected override Task SeedAsync(PoolableDbContext context) + => context.Database.EnsureCreatedResilientlyAsync(); public DbContext CreateContext(int minBatchSize) { diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs index 771b6bc5b..a873c85b4 100644 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs @@ -1,7 +1,6 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations.Schema; using System.Net.NetworkInformation; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL; @@ -95,20 +94,20 @@ public virtual void Can_query_using_any_mapped_data_type() // CharAsChar1 = 'f', CharAsText = 'g', CharAsVarchar = 'h', - BytesAsBytea = new byte[] { 86 }, + BytesAsBytea = [86], GuidAsUuid = new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), EnumAsText = StringEnum16.Value4, EnumAsVarchar = StringEnumU16.Value4, PhysicalAddressAsMacaddr = PhysicalAddress.Parse("08-00-2B-01-02-03"), NpgsqlPointAsPoint = new NpgsqlPoint(5.2, 3.3), - StringAsJsonb = @"{""a"": ""b""}", - StringAsJson = @"{""a"": ""b""}", + StringAsJsonb = """{"a": "b"}""", + StringAsJson = """{"a": "b"}""", DictionaryAsHstore = new Dictionary { { "a", "b" } }, ImmutableDictionaryAsHstore = ImmutableDictionary.Empty.Add("c", "d"), NpgsqlRangeAsRange = new NpgsqlRange(4, true, 8, false), - IntArrayAsIntArray = new[] { 2, 3 }, + IntArrayAsIntArray = [2, 3], PhysicalAddressArrayAsMacaddrArray = - new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }, + [PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04")], UintAsXid = (uint)int.MaxValue + 1, #pragma warning disable CS0618 // Full-text search client-parsing is obsolete @@ -238,7 +237,7 @@ public virtual void Can_query_using_any_mapped_data_type() // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Point == param29)); // ReSharper disable once ConvertToConstant.Local - var param30 = @"{""a"": ""b""}"; + var param30 = """{"a": "b"}"""; Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsJsonb == param30)); // operator does not exist: json = json (https://stackoverflow.com/questions/32843213/operator-does-not-exist-json-json) @@ -467,7 +466,8 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types() var parameters = DumpParameters(); Assert.Equal( - @"@p0='77' + """ +@p0='77' @p1='True' @p2='80' (DbType = Int16) @p3='0x56' (Nullable = false) @@ -500,8 +500,8 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types() @p28=''a' & 'b'' (Nullable = false) (DbType = Object) @p29=''a' 'b'' (Nullable = false) (DbType = Object) @p30='79' -@p31='{""a"": ""b""}' (Nullable = false) (DbType = Object) -@p32='{""a"": ""b""}' (Nullable = false) (DbType = Object) +@p31='{"a": "b"}' (Nullable = false) (DbType = Object) +@p32='{"a": "b"}' (Nullable = false) (DbType = Object) @p33='Gumball Rules!' (Nullable = false) @p34='Gumball Rules OK' (Nullable = false) @p35='11:15:12' (DbType = Object) @@ -511,7 +511,8 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types() @p39='4294967295' @p40='-1' @p41='2147483648' (DbType = Object) -@p42='-1'", +@p42='-1' +""", parameters, ignoreLineEndingDifferences: true); } @@ -555,7 +556,7 @@ private static void AssertMappedDataTypes(MappedDataTypes entity, int id) // Assert.Equal('f', entity.CharAsChar1); Assert.Equal('g', entity.CharAsText); Assert.Equal('h', entity.CharAsVarchar); - Assert.Equal(new byte[] { 86 }, entity.BytesAsBytea); + Assert.Equal([86], entity.BytesAsBytea); Assert.Equal(new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), entity.GuidAsUuid); @@ -564,14 +565,14 @@ private static void AssertMappedDataTypes(MappedDataTypes entity, int id) Assert.Equal(PhysicalAddress.Parse("08-00-2B-01-02-03"), entity.PhysicalAddressAsMacaddr); Assert.Equal(new NpgsqlPoint(5.2, 3.3), entity.NpgsqlPointAsPoint); - Assert.Equal(@"{""a"": ""b""}", entity.StringAsJsonb); - Assert.Equal(@"{""a"": ""b""}", entity.StringAsJson); + Assert.Equal("""{"a": "b"}""", entity.StringAsJsonb); + Assert.Equal("""{"a": "b"}""", entity.StringAsJson); Assert.Equal(new Dictionary { { "a", "b" } }, entity.DictionaryAsHstore); Assert.Equal(new NpgsqlRange(4, true, 8, false), entity.NpgsqlRangeAsRange); Assert.Equal(new[] { 2, 3 }, entity.IntArrayAsIntArray); Assert.Equal( - new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }, + [PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04")], entity.PhysicalAddressArrayAsMacaddrArray); Assert.Equal((uint)int.MaxValue + 1, entity.UintAsXid); @@ -613,19 +614,19 @@ private static MappedDataTypes CreateMappedDataTypes(int id) // CharAsChar1 = 'f', CharAsText = 'g', CharAsVarchar = 'h', - BytesAsBytea = new byte[] { 86 }, + BytesAsBytea = [86], GuidAsUuid = new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), EnumAsText = StringEnum16.Value4, EnumAsVarchar = StringEnumU16.Value4, PhysicalAddressAsMacaddr = PhysicalAddress.Parse("08-00-2B-01-02-03"), NpgsqlPointAsPoint = new NpgsqlPoint(5.2, 3.3), - StringAsJsonb = @"{""a"": ""b""}", - StringAsJson = @"{""a"": ""b""}", + StringAsJsonb = """{"a": "b"}""", + StringAsJson = """{"a": "b"}""", DictionaryAsHstore = new Dictionary { { "a", "b" } }, NpgsqlRangeAsRange = new NpgsqlRange(4, true, 8, false), - IntArrayAsIntArray = new[] { 2, 3 }, + IntArrayAsIntArray = [2, 3], PhysicalAddressArrayAsMacaddrArray = - new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }, + [PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04")], UintAsXid = (uint)int.MaxValue + 1, #pragma warning disable CS0618 // Full-text search client-parsing is obsolete SearchQuery = NpgsqlTsQuery.Parse("a & b"), @@ -717,88 +718,102 @@ private static void AssertNullMappedNullableDataTypes(MappedNullableDataTypes en Assert.Null(entity.Mood); } - public override void Can_query_with_null_parameters_using_any_nullable_data_type() + public override async Task Can_query_with_null_parameters_using_any_nullable_data_type() { using (var context = CreateContext()) { context.Set().Add( new BuiltInNullableDataTypes { Id = 711 }); - Assert.Equal(1, context.SaveChanges()); + Assert.Equal(1, await context.SaveChangesAsync()); } using (var context = CreateContext()) { - var entity = context.Set().Where(e => e.Id == 711).ToList().Single(); + var entity = (await context.Set().Where(e => e.Id == 711).ToListAsync()).Single(); short? param1 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableInt16 == param1).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt16 == param1).ToListAsync()) + .Single()); Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && (long?)e.TestNullableInt16 == param1).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && (long?)e.TestNullableInt16 == param1) + .ToListAsync()) + .Single()); int? param2 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableInt32 == param2).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt32 == param2).ToListAsync()) + .Single()); long? param3 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableInt64 == param3).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt64 == param3).ToListAsync()) + .Single()); double? param4 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableDouble == param4).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDouble == param4).ToListAsync()) + .Single()); decimal? param5 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableDecimal == param5).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDecimal == param5).ToListAsync()) + .Single()); DateTime? param6 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableDateTime == param6).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDateTime == param6).ToListAsync()) + .Single()); // We don't support DateTimeOffset TimeSpan? param8 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableTimeSpan == param8).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableTimeSpan == param8).ToListAsync()) + .Single()); float? param9 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableSingle == param9).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableSingle == param9).ToListAsync()) + .Single()); bool? param10 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableBoolean == param10).ToList().Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableBoolean == param10).ToListAsync()) + .Single()); // We don't support byte Enum64? param12 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.Enum64 == param12).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum64 == param12).ToListAsync()).Single()); Enum32? param13 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.Enum32 == param13).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum32 == param13).ToListAsync()).Single()); Enum16? param14 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.Enum16 == param14).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum16 == param14).ToListAsync()).Single()); Enum8? param15 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.Enum8 == param15).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum8 == param15).ToListAsync()).Single()); var entityType = context.Model.FindEntityType(typeof(BuiltInNullableDataTypes)); if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt16)) is not null) @@ -806,8 +821,9 @@ public override void Can_query_with_null_parameters_using_any_nullable_data_type ushort? param16 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt16 == param16).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt16 == param16) + .ToListAsync()) + .Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt32)) is not null) @@ -815,8 +831,9 @@ public override void Can_query_with_null_parameters_using_any_nullable_data_type uint? param17 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt32 == param17).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt32 == param17) + .ToListAsync()) + .Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt64)) is not null) @@ -824,8 +841,9 @@ public override void Can_query_with_null_parameters_using_any_nullable_data_type ulong? param18 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt64 == param18).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt64 == param18) + .ToListAsync()) + .Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableCharacter)) is not null) @@ -833,8 +851,9 @@ public override void Can_query_with_null_parameters_using_any_nullable_data_type char? param19 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableCharacter == param19).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableCharacter == param19) + .ToListAsync()) + .Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableSignedByte)) is not null) @@ -842,57 +861,68 @@ public override void Can_query_with_null_parameters_using_any_nullable_data_type sbyte? param20 = null; Assert.Same( entity, - context.Set().Where(e => e.Id == 711 && e.TestNullableSignedByte == param20).ToList() - .Single()); + (await context.Set().Where(e => e.Id == 711 && e.TestNullableSignedByte == param20) + .ToListAsync()) + .Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.EnumU64)) is not null) { EnumU64? param21 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.EnumU64 == param21).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.EnumU64 == param21).ToListAsync()).Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.EnumU32)) is not null) { EnumU32? param22 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.EnumU32 == param22).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.EnumU32 == param22).ToListAsync()).Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.EnumU16)) is not null) { EnumU16? param23 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.EnumU16 == param23).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.EnumU16 == param23).ToListAsync()).Single()); } if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.EnumS8)) is not null) { EnumS8? param24 = null; Assert.Same( - entity, context.Set().Where(e => e.Id == 711 && e.EnumS8 == param24).ToList().Single()); + entity, + (await context.Set().Where(e => e.Id == 711 && e.EnumS8 == param24).ToListAsync()).Single()); } } } [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_all_nullable_data_types_with_values_set_to_non_null() { } + public override Task Can_insert_and_read_back_all_nullable_data_types_with_values_set_to_non_null() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_non_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_non_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_object_backed_data_types() { } + public override Task Can_insert_and_read_back_object_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_query_using_any_data_type_nullable_shadow() { } + public override Task Can_query_using_any_data_type_nullable_shadow() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_query_using_any_data_type_shadow() { } + public override Task Can_query_using_any_data_type_shadow() + => Task.CompletedTask; [ConditionalFact] public void Sum_Conversions() @@ -962,8 +992,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 +1003,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", new[] { "happy", "sad" }); - MakeRequired(modelBuilder); // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't @@ -1006,25 +1028,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasColumnType("timestamp without time zone"); // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data - var builtInDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - builtInDataTypes[nameof(BuiltInDataTypes.TestDateTimeOffset)] + modelBuilder.Entity().Metadata.GetSeedData() + .Single(t => (int)t[nameof(BuiltInDataTypes.Id)] == 13)[nameof(BuiltInDataTypes.TestDateTimeOffset)] = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.Zero); - var builtInNullableDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - builtInNullableDataTypes[nameof(BuiltInNullableDataTypes.TestNullableDateTimeOffset)] + modelBuilder.Entity().Metadata.GetSeedData() + .Single(t => (int)t[nameof(BuiltInDataTypes.Id)] == 13)[nameof(BuiltInNullableDataTypes.TestNullableDateTimeOffset)] = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.Zero); - var objectBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - objectBackedDataTypes[nameof(ObjectBackedDataTypes.DateTimeOffset)] - = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(ObjectBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); - var nullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - nullableBackedDataTypes[nameof(NullableBackedDataTypes.DateTimeOffset)] + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(NullableBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.Zero); - var nonNullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - nonNullableBackedDataTypes[nameof(NonNullableBackedDataTypes.DateTimeOffset)] - = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(NonNullableBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); modelBuilder.Entity( b => diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs index bfe7cf2a0..7bf72a670 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs @@ -3,14 +3,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update; -public class ComplexTypeBulkUpdatesNpgsqlTest : ComplexTypeBulkUpdatesTestBase< - ComplexTypeBulkUpdatesNpgsqlTest.ComplexTypeBulkUpdatesNpgsqlFixture> +public class ComplexTypeBulkUpdatesNpgsqlTest( + ComplexTypeBulkUpdatesNpgsqlTest.ComplexTypeBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : ComplexTypeBulkUpdatesRelationalTestBase(fixture, testOutputHelper) { - public ComplexTypeBulkUpdatesNpgsqlTest(ComplexTypeBulkUpdatesNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_entity_type_with_complex_type(bool async) { await base.Delete_entity_type_with_complex_type(async); @@ -22,9 +19,9 @@ DELETE FROM "Customer" AS c """); } - public override async Task Delete_complex_type_throws(bool async) + public override async Task Delete_complex_type(bool async) { - await base.Delete_complex_type_throws(async); + await base.Delete_complex_type(async); AssertSql(); } @@ -90,13 +87,135 @@ public override async Task Update_multiple_projected_complex_types_via_anonymous """); } - public override async Task Update_projected_complex_type_via_OrderBy_Skip_throws(bool async) + public override async Task Update_projected_complex_type_via_OrderBy_Skip(bool async) { - await base.Update_projected_complex_type_via_OrderBy_Skip_throws(async); + await base.Update_projected_complex_type_via_OrderBy_Skip(async); AssertExecuteUpdateSql(); } + public override async Task Update_complex_type_to_parameter(bool async) + { + await base.Update_complex_type_to_parameter(async); + + AssertExecuteUpdateSql( + """ +@__complex_type_newAddress_0_AddressLine1='New AddressLine1' +@__complex_type_newAddress_0_AddressLine2='New AddressLine2' +@__complex_type_newAddress_0_Tags={ 'new_tag1', 'new_tag2' } (DbType = Object) +@__complex_type_newAddress_0_ZipCode='99999' (Nullable = true) +@__complex_type_newAddress_0_Code='FR' +@__complex_type_newAddress_0_FullName='France' + +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = @__complex_type_newAddress_0_AddressLine1, + "ShippingAddress_AddressLine2" = @__complex_type_newAddress_0_AddressLine2, + "ShippingAddress_Tags" = @__complex_type_newAddress_0_Tags, + "ShippingAddress_ZipCode" = @__complex_type_newAddress_0_ZipCode, + "ShippingAddress_Country_Code" = @__complex_type_newAddress_0_Code, + "ShippingAddress_Country_FullName" = @__complex_type_newAddress_0_FullName +"""); + } + + public override async Task Update_nested_complex_type_to_parameter(bool async) + { + await base.Update_nested_complex_type_to_parameter(async); + + AssertExecuteUpdateSql( + """ +@__complex_type_newCountry_0_Code='FR' +@__complex_type_newCountry_0_FullName='France' + +UPDATE "Customer" AS c +SET "ShippingAddress_Country_Code" = @__complex_type_newCountry_0_Code, + "ShippingAddress_Country_FullName" = @__complex_type_newCountry_0_FullName +"""); + } + + public override async Task Update_complex_type_to_another_database_complex_type(bool async) + { + await base.Update_complex_type_to_another_database_complex_type(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = c."BillingAddress_AddressLine1", + "ShippingAddress_AddressLine2" = c."BillingAddress_AddressLine2", + "ShippingAddress_Tags" = c."BillingAddress_Tags", + "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode", + "ShippingAddress_Country_Code" = c."ShippingAddress_Country_Code", + "ShippingAddress_Country_FullName" = c."ShippingAddress_Country_FullName" +"""); + } + + public override async Task Update_complex_type_to_inline_without_lambda(bool async) + { + await base.Update_complex_type_to_inline_without_lambda(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = 'New AddressLine1', + "ShippingAddress_AddressLine2" = 'New AddressLine2', + "ShippingAddress_Tags" = ARRAY['new_tag1','new_tag2']::text[], + "ShippingAddress_ZipCode" = 99999, + "ShippingAddress_Country_Code" = 'FR', + "ShippingAddress_Country_FullName" = 'France' +"""); + } + + public override async Task Update_complex_type_to_inline_with_lambda(bool async) + { + await base.Update_complex_type_to_inline_with_lambda(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = 'New AddressLine1', + "ShippingAddress_AddressLine2" = 'New AddressLine2', + "ShippingAddress_Tags" = ARRAY['new_tag1','new_tag2']::text[], + "ShippingAddress_ZipCode" = 99999, + "ShippingAddress_Country_Code" = 'FR', + "ShippingAddress_Country_FullName" = 'France' +"""); + } + + public override async Task Update_complex_type_to_another_database_complex_type_with_subquery(bool async) + { + await base.Update_complex_type_to_another_database_complex_type_with_subquery(async); + + AssertExecuteUpdateSql( + """ +@__p_0='1' + +UPDATE "Customer" AS c0 +SET "ShippingAddress_AddressLine1" = c1."BillingAddress_AddressLine1", + "ShippingAddress_AddressLine2" = c1."BillingAddress_AddressLine2", + "ShippingAddress_Tags" = c1."BillingAddress_Tags", + "ShippingAddress_ZipCode" = c1."BillingAddress_ZipCode", + "ShippingAddress_Country_Code" = c1."ShippingAddress_Country_Code", + "ShippingAddress_Country_FullName" = c1."ShippingAddress_Country_FullName" +FROM ( + SELECT c."Id", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" + FROM "Customer" AS c + ORDER BY c."Id" NULLS FIRST + OFFSET @__p_0 +) AS c1 +WHERE c0."Id" = c1."Id" +"""); + } + + public override async Task Update_collection_inside_complex_type(bool async) + { + await base.Update_collection_inside_complex_type(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Customer" AS c +SET "ShippingAddress_Tags" = ARRAY['new_tag1','new_tag2']::text[] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); @@ -110,7 +229,7 @@ private void AssertSql(params string[] expected) protected void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); - public class ComplexTypeBulkUpdatesNpgsqlFixture : ComplexTypeBulkUpdatesFixtureBase + public class ComplexTypeBulkUpdatesNpgsqlFixture : ComplexTypeBulkUpdatesRelationalFixtureBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs index 943d6c8f9..d32b3c4f7 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs @@ -3,7 +3,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update; -public class NonSharedModelBulkUpdatesNpgsqlTest : NonSharedModelBulkUpdatesTestBase +public class NonSharedModelBulkUpdatesNpgsqlTest : NonSharedModelBulkUpdatesRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; @@ -22,6 +22,24 @@ DELETE FROM "Owner" AS o """); } + public override async Task Delete_with_owned_collection_and_non_natively_translatable_query(bool async) + { + await base.Delete_with_owned_collection_and_non_natively_translatable_query(async); + + AssertSql( + """ +@__p_0='1' + +DELETE FROM "Owner" AS o +WHERE o."Id" IN ( + SELECT o0."Id" + FROM "Owner" AS o0 + ORDER BY o0."Title" NULLS FIRST + OFFSET @__p_0 +) +"""); + } + public override async Task Delete_aggregate_root_when_table_sharing_with_owned(bool async) { await base.Delete_aggregate_root_when_table_sharing_with_owned(async); @@ -32,6 +50,19 @@ DELETE FROM "Owner" AS o """); } + public override async Task Replace_ColumnExpression_in_column_setter(bool async) + { + await base.Replace_ColumnExpression_in_column_setter(async); + + AssertSql( + """ +UPDATE "OwnedCollection" AS o0 +SET "Value" = 'SomeValue' +FROM "Owner" AS o +WHERE o."Id" = o0."OwnerId" +"""); + } + public override async Task Delete_aggregate_root_when_table_sharing_with_non_owned_throws(bool async) { await base.Delete_aggregate_root_when_table_sharing_with_non_owned_throws(async); @@ -77,6 +108,19 @@ public override async Task Update_non_owned_property_on_entity_with_owned2(bool """); } + public override async Task Update_non_owned_property_on_entity_with_owned_in_join(bool async) + { + await base.Update_non_owned_property_on_entity_with_owned_in_join(async); + + AssertSql( + """ +UPDATE "Owner" AS o +SET "Title" = 'NewValue' +FROM "Owner" AS o0 +WHERE o."Id" = o0."Id" +"""); + } + public override async Task Update_owned_and_non_owned_properties_with_table_sharing(bool async) { await base.Update_owned_and_non_owned_properties_with_table_sharing(async); @@ -85,7 +129,7 @@ public override async Task Update_owned_and_non_owned_properties_with_table_shar """ UPDATE "Owner" AS o SET "OwnedReference_Number" = length(o."Title")::int, - "Title" = o."OwnedReference_Number"::text + "Title" = COALESCE(o."OwnedReference_Number"::text, '') """); } @@ -101,10 +145,10 @@ public override async Task Update_main_table_in_entity_with_entity_splitting(boo tb.Property(b => b.Title); tb.Property(b => b.Rating); }), - seed: context => + seed: async context => { context.Set().Add(new Blog { Title = "SomeBlog" }); - context.SaveChanges(); + await context.SaveChangesAsync(); }); await AssertUpdate( @@ -130,11 +174,25 @@ public override async Task Update_non_main_table_in_entity_with_entity_splitting UPDATE "BlogsPart1" AS b0 SET "Rating" = length(b0."Title")::int, "Title" = b0."Rating"::text +FROM "Blogs" AS b +WHERE b."Id" = b0."Id" """); } - public override Task Delete_entity_with_auto_include(bool async) - => Assert.ThrowsAsync(() => base.Delete_entity_with_auto_include(async)); // #30577 + public override async Task Delete_entity_with_auto_include(bool async) + { + await base.Delete_entity_with_auto_include(async); + + AssertSql( + """ +DELETE FROM "Context30572_Principal" AS c +WHERE c."Id" IN ( + SELECT c0."Id" + FROM "Context30572_Principal" AS c0 + LEFT JOIN "Context30572_Dependent" AS c1 ON c0."DependentId" = c1."Id" +) +"""); + } public override async Task Update_with_alias_uniquification_in_setter_subquery(bool async) { @@ -151,6 +209,36 @@ SELECT COALESCE(sum(o0."Amount"), 0)::int """); } + [ConditionalTheory] // #3001 + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_with_primitive_collection_in_value_selector(bool async) + { + var contextFactory = await InitializeAsync( + seed: async ctx => + { + ctx.AddRange(new EntityWithPrimitiveCollection { Tags = ["tag1", "tag2"] }); + await ctx.SaveChangesAsync(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.EntitiesWithPrimitiveCollection, + s => s.SetProperty(x => x.Tags, x => x.Tags.Append("another_tag")), + rowsAffectedCount: 1); + } + + protected class Context3001(DbContextOptions options) : DbContext(options) + { + public DbSet EntitiesWithPrimitiveCollection { get; set; } + } + + protected class EntityWithPrimitiveCollection + { + public int Id { get; set; } + public List Tags { get; set; } + } + private void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs index 9cb600c39..13ea83511 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs @@ -5,8 +5,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class NorthwindBulkUpdatesNpgsqlFixture : NorthwindBulkUpdatesFixture - where TModelCustomizer : IModelCustomizer, new() +public class NorthwindBulkUpdatesNpgsqlFixture : NorthwindBulkUpdatesRelationalFixture + where TModelCustomizer : ITestModelCustomizer, new() { protected override ITestStoreFactory TestStoreFactory => NpgsqlNorthwindTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs index 098e57b2e..40d60f7f5 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs @@ -3,15 +3,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class NorthwindBulkUpdatesNpgsqlTest : NorthwindBulkUpdatesTestBase> +public class NorthwindBulkUpdatesNpgsqlTest( + NorthwindBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : NorthwindBulkUpdatesRelationalTestBase>(fixture, testOutputHelper) { - public NorthwindBulkUpdatesNpgsqlTest( - NorthwindBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_Where_TagWith(bool async) { await base.Delete_Where_TagWith(async); @@ -80,13 +76,13 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 ORDER BY o0."OrderID" NULLS FIRST OFFSET @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -102,13 +98,13 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 ORDER BY o0."OrderID" NULLS FIRST LIMIT @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -124,13 +120,13 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 ORDER BY o0."OrderID" NULLS FIRST LIMIT @__p_0 OFFSET @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -146,12 +142,12 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 OFFSET @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -167,12 +163,12 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 LIMIT @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -188,12 +184,12 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10300 LIMIT @__p_0 OFFSET @__p_0 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS o1 + WHERE o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -266,16 +262,16 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT t."OrderID", t."ProductID", t."Discount", t."Quantity", t."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" - FROM "Order Details" AS o0 - WHERE o0."OrderID" < 10300 + SELECT o1."OrderID", o1."ProductID" + FROM "Order Details" AS o1 + WHERE o1."OrderID" < 10300 LIMIT @__p_0 OFFSET @__p_0 - ) AS t + ) AS o0 LIMIT @__p_2 OFFSET @__p_1 - ) AS t0 - WHERE t0."OrderID" = o."OrderID" AND t0."ProductID" = o."ProductID") + ) AS o2 + WHERE o2."OrderID" = o."OrderID" AND o2."ProductID" = o."ProductID") """); } @@ -313,11 +309,11 @@ WHERE EXISTS ( SELECT 1 FROM "Orders" AS o0 INNER JOIN ( - SELECT o1."OrderID", o1."ProductID", o1."Discount", o1."Quantity", o1."UnitPrice" - FROM "Order Details" AS o1 - WHERE o1."ProductID" > 0 - ) AS t ON o0."OrderID" = t."OrderID" - WHERE o0."OrderID" < 10250 AND t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + SELECT o2."OrderID", o2."ProductID" + FROM "Order Details" AS o2 + WHERE o2."ProductID" > 0 + ) AS o1 ON o0."OrderID" = o1."OrderID" + WHERE o0."OrderID" < 10250 AND o1."OrderID" = o."OrderID" AND o1."ProductID" = o."ProductID") """); } @@ -366,8 +362,8 @@ WHERE o0."OrderID" < 10250 SELECT o1."OrderID", o1."ProductID", o1."Discount", o1."Quantity", o1."UnitPrice" FROM "Order Details" AS o1 WHERE o1."OrderID" > 11250 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS u + WHERE u."OrderID" = o."OrderID" AND u."ProductID" = o."ProductID") """); } @@ -381,15 +377,15 @@ DELETE FROM "Order Details" AS o WHERE EXISTS ( SELECT 1 FROM ( - SELECT o0."OrderID", o0."ProductID", o0."Discount", o0."Quantity", o0."UnitPrice" + SELECT o0."OrderID", o0."ProductID" FROM "Order Details" AS o0 WHERE o0."OrderID" < 10250 UNION ALL - SELECT o1."OrderID", o1."ProductID", o1."Discount", o1."Quantity", o1."UnitPrice" + SELECT o1."OrderID", o1."ProductID" FROM "Order Details" AS o1 WHERE o1."OrderID" > 11250 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS u + WHERE u."OrderID" = o."OrderID" AND u."ProductID" = o."ProductID") """); } @@ -410,8 +406,8 @@ WHERE o0."OrderID" < 10250 SELECT o1."OrderID", o1."ProductID", o1."Discount", o1."Quantity", o1."UnitPrice" FROM "Order Details" AS o1 WHERE o1."OrderID" > 11250 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS i + WHERE i."OrderID" = o."OrderID" AND i."ProductID" = o."ProductID") """); } @@ -432,8 +428,8 @@ WHERE o0."OrderID" < 10250 SELECT o1."OrderID", o1."ProductID", o1."Discount", o1."Quantity", o1."UnitPrice" FROM "Order Details" AS o1 WHERE o1."OrderID" > 11250 - ) AS t - WHERE t."OrderID" = o."OrderID" AND t."ProductID" = o."ProductID") + ) AS e + WHERE e."OrderID" = o."OrderID" AND e."ProductID" = o."ProductID") """); } @@ -503,13 +499,13 @@ public override async Task Delete_with_join(bool async) DELETE FROM "Order Details" AS o USING ( - SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" + SELECT o0."OrderID" FROM "Orders" AS o0 WHERE o0."OrderID" < 10300 ORDER BY o0."OrderID" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 -) AS t -WHERE o."OrderID" = t."OrderID" +) AS o1 +WHERE o."OrderID" = o1."OrderID" """); } @@ -527,12 +523,12 @@ WHERE EXISTS ( SELECT 1 FROM "Order Details" AS o0 LEFT JOIN ( - SELECT o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" - FROM "Orders" AS o1 - WHERE o1."OrderID" < 10300 - ORDER BY o1."OrderID" NULLS FIRST + SELECT o2."OrderID" + FROM "Orders" AS o2 + WHERE o2."OrderID" < 10300 + ORDER BY o2."OrderID" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 - ) AS t ON o0."OrderID" = t."OrderID" + ) AS o1 ON o0."OrderID" = o1."OrderID" WHERE o0."OrderID" < 10276 AND o0."OrderID" = o."OrderID" AND o0."ProductID" = o."ProductID") """); } @@ -548,12 +544,12 @@ WHERE EXISTS ( SELECT 1 FROM "Order Details" AS o0 CROSS JOIN ( - SELECT o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" - FROM "Orders" AS o1 - WHERE o1."OrderID" < 10300 - ORDER BY o1."OrderID" NULLS FIRST + SELECT 1 + FROM "Orders" AS o2 + WHERE o2."OrderID" < 10300 + ORDER BY o2."OrderID" NULLS FIRST LIMIT 100 OFFSET 0 - ) AS t + ) AS o1 WHERE o0."OrderID" < 10276 AND o0."OrderID" = o."OrderID" AND o0."ProductID" = o."ProductID") """); } @@ -569,12 +565,12 @@ WHERE EXISTS ( SELECT 1 FROM "Order Details" AS o0 JOIN LATERAL ( - SELECT o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" - FROM "Orders" AS o1 - WHERE o1."OrderID" < o0."OrderID" - ORDER BY o1."OrderID" NULLS FIRST + SELECT 1 + FROM "Orders" AS o2 + WHERE o2."OrderID" < o0."OrderID" + ORDER BY o2."OrderID" NULLS FIRST LIMIT 100 OFFSET 0 - ) AS t ON TRUE + ) AS o1 ON TRUE WHERE o0."OrderID" < 10276 AND o0."OrderID" = o."OrderID" AND o0."ProductID" = o."ProductID") """); } @@ -590,12 +586,12 @@ WHERE EXISTS ( SELECT 1 FROM "Order Details" AS o0 LEFT JOIN LATERAL ( - SELECT o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" - FROM "Orders" AS o1 - WHERE o1."OrderID" < o0."OrderID" - ORDER BY o1."OrderID" NULLS FIRST + SELECT 1 + FROM "Orders" AS o2 + WHERE o2."OrderID" < o0."OrderID" + ORDER BY o2."OrderID" NULLS FIRST LIMIT 100 OFFSET 0 - ) AS t ON TRUE + ) AS o1 ON TRUE WHERE o0."OrderID" < 10276 AND o0."OrderID" = o."OrderID" AND o0."ProductID" = o."ProductID") """); } @@ -732,14 +728,14 @@ public override async Task Update_Where_OrderBy_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + SELECT c."CustomerID" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' +) AS c1 +WHERE c0."CustomerID" = c1."CustomerID" """); } @@ -751,16 +747,16 @@ public override async Task Update_Where_OrderBy_Skip_set_constant(bool async) """ @__p_0='4' -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - ORDER BY c0."City" NULLS FIRST + SELECT c."CustomerID" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + ORDER BY c."City" NULLS FIRST OFFSET @__p_0 -) AS t -WHERE c."CustomerID" = t."CustomerID" +) AS c1 +WHERE c0."CustomerID" = c1."CustomerID" """); } @@ -772,16 +768,16 @@ public override async Task Update_Where_OrderBy_Take_set_constant(bool async) """ @__p_0='4' -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - ORDER BY c0."City" NULLS FIRST + SELECT c."CustomerID" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + ORDER BY c."City" NULLS FIRST LIMIT @__p_0 -) AS t -WHERE c."CustomerID" = t."CustomerID" +) AS c1 +WHERE c0."CustomerID" = c1."CustomerID" """); } @@ -794,16 +790,16 @@ public override async Task Update_Where_OrderBy_Skip_Take_set_constant(bool asyn @__p_1='4' @__p_0='2' -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - ORDER BY c0."City" NULLS FIRST + SELECT c."CustomerID" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + ORDER BY c."City" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 -) AS t -WHERE c."CustomerID" = t."CustomerID" +) AS c1 +WHERE c0."CustomerID" = c1."CustomerID" """); } @@ -816,21 +812,21 @@ public override async Task Update_Where_OrderBy_Skip_Take_Skip_Take_set_constant @__p_1='6' @__p_0='2' -UPDATE "Customers" AS c +UPDATE "Customers" AS c1 SET "ContactName" = 'Updated' FROM ( - SELECT t."CustomerID", t."Address", t."City", t."CompanyName", t."ContactName", t."ContactTitle", t."Country", t."Fax", t."Phone", t."PostalCode", t."Region" + SELECT c0."CustomerID" FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - ORDER BY c0."City" NULLS FIRST + SELECT c."CustomerID", c."City" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + ORDER BY c."City" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 - ) AS t - ORDER BY t."City" NULLS FIRST + ) AS c0 + ORDER BY c0."City" NULLS FIRST LIMIT @__p_0 OFFSET @__p_0 -) AS t0 -WHERE c."CustomerID" = t0."CustomerID" +) AS c2 +WHERE c1."CustomerID" = c2."CustomerID" """); } @@ -907,14 +903,14 @@ public override async Task Update_Where_Distinct_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT DISTINCT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + SELECT DISTINCT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' +) AS c1 +WHERE c0."CustomerID" = c1."CustomerID" """); } @@ -924,15 +920,15 @@ public override async Task Update_Where_using_navigation_set_null(bool async) AssertExecuteUpdateSql( """ -UPDATE "Orders" AS o +UPDATE "Orders" AS o0 SET "OrderDate" = NULL FROM ( - SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", c."CustomerID" AS "CustomerID0" - FROM "Orders" AS o0 - LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" + SELECT o."OrderID" + FROM "Orders" AS o + LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" WHERE c."City" = 'Seattle' -) AS t -WHERE o."OrderID" = t."OrderID" +) AS s +WHERE o0."OrderID" = s."OrderID" """); } @@ -1081,18 +1077,18 @@ public override async Task Update_Union_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c1 SET "ContactName" = 'Updated' FROM ( + SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + UNION SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - UNION - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."CustomerID" LIKE 'A%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + WHERE c0."CustomerID" LIKE 'A%' +) AS u +WHERE c1."CustomerID" = u."CustomerID" """); } @@ -1102,18 +1098,18 @@ public override async Task Update_Concat_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c1 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" - FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' + SELECT c."CustomerID" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' UNION ALL - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."CustomerID" LIKE 'A%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + SELECT c0."CustomerID" + FROM "Customers" AS c0 + WHERE c0."CustomerID" LIKE 'A%' +) AS u +WHERE c1."CustomerID" = u."CustomerID" """); } @@ -1123,18 +1119,18 @@ public override async Task Update_Except_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c1 SET "ContactName" = 'Updated' FROM ( + SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + EXCEPT SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - EXCEPT - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."CustomerID" LIKE 'A%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + WHERE c0."CustomerID" LIKE 'A%' +) AS e +WHERE c1."CustomerID" = e."CustomerID" """); } @@ -1144,18 +1140,18 @@ public override async Task Update_Intersect_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c1 SET "ContactName" = 'Updated' FROM ( + SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" + FROM "Customers" AS c + WHERE c."CustomerID" LIKE 'F%' + INTERSECT SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM "Customers" AS c0 - WHERE c0."CustomerID" LIKE 'F%' - INTERSECT - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."CustomerID" LIKE 'A%' -) AS t -WHERE c."CustomerID" = t."CustomerID" + WHERE c0."CustomerID" LIKE 'A%' +) AS i +WHERE c1."CustomerID" = i."CustomerID" """); } @@ -1168,11 +1164,11 @@ public override async Task Update_with_join_set_constant(bool async) UPDATE "Customers" AS c SET "ContactName" = 'Updated' FROM ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10300 -) AS t -WHERE c."CustomerID" = t."CustomerID" AND c."CustomerID" LIKE 'F%' +) AS o0 +WHERE c."CustomerID" = o0."CustomerID" AND c."CustomerID" LIKE 'F%' """); } @@ -1182,19 +1178,19 @@ public override async Task Update_with_left_join_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t."OrderID", t."CustomerID" AS "CustomerID0", t."EmployeeID", t."OrderDate" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c LEFT JOIN ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10300 - ) AS t ON c0."CustomerID" = t."CustomerID" - WHERE c0."CustomerID" LIKE 'F%' -) AS t0 -WHERE c."CustomerID" = t0."CustomerID" + ) AS o0 ON c."CustomerID" = o0."CustomerID" + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c0."CustomerID" = s."CustomerID" """); } @@ -1207,10 +1203,10 @@ public override async Task Update_with_cross_join_set_constant(bool async) UPDATE "Customers" AS c SET "ContactName" = 'Updated' FROM ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT 1 FROM "Orders" AS o WHERE o."OrderID" < 10300 -) AS t +) AS o0 WHERE c."CustomerID" LIKE 'F%' """); } @@ -1221,19 +1217,19 @@ public override async Task Update_with_cross_apply_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t."OrderID", t."CustomerID" AS "CustomerID0", t."EmployeeID", t."OrderDate" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c JOIN LATERAL ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT 1 FROM "Orders" AS o - WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c0."ContactName")::int - ) AS t ON TRUE - WHERE c0."CustomerID" LIKE 'F%' -) AS t0 -WHERE c."CustomerID" = t0."CustomerID" + WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c."ContactName")::int + ) AS o0 ON TRUE + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c0."CustomerID" = s."CustomerID" """); } @@ -1243,19 +1239,19 @@ public override async Task Update_with_outer_apply_set_constant(bool async) AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c0 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t."OrderID", t."CustomerID" AS "CustomerID0", t."EmployeeID", t."OrderDate" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c LEFT JOIN LATERAL ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT 1 FROM "Orders" AS o - WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c0."ContactName")::int - ) AS t ON TRUE - WHERE c0."CustomerID" LIKE 'F%' -) AS t0 -WHERE c."CustomerID" = t0."CustomerID" + WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c."ContactName")::int + ) AS o0 ON TRUE + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c0."CustomerID" = s."CustomerID" """); } @@ -1266,24 +1262,24 @@ public override async Task Update_with_cross_join_left_join_set_constant(bool as AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c2 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t."CustomerID" AS "CustomerID0", t."Address" AS "Address0", t."City" AS "City0", t."CompanyName" AS "CompanyName0", t."ContactName" AS "ContactName0", t."ContactTitle" AS "ContactTitle0", t."Country" AS "Country0", t."Fax" AS "Fax0", t."Phone" AS "Phone0", t."PostalCode" AS "PostalCode0", t."Region" AS "Region0", t0."OrderID", t0."CustomerID" AS "CustomerID1", t0."EmployeeID", t0."OrderDate" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c CROSS JOIN ( - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."City" LIKE 'S%' - ) AS t + SELECT 1 + FROM "Customers" AS c0 + WHERE c0."City" LIKE 'S%' + ) AS c1 LEFT JOIN ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10300 - ) AS t0 ON c0."CustomerID" = t0."CustomerID" - WHERE c0."CustomerID" LIKE 'F%' -) AS t1 -WHERE c."CustomerID" = t1."CustomerID" + ) AS o0 ON c."CustomerID" = o0."CustomerID" + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c2."CustomerID" = s."CustomerID" """); } @@ -1294,24 +1290,24 @@ public override async Task Update_with_cross_join_cross_apply_set_constant(bool AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c2 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t0."OrderID", t0."CustomerID" AS "CustomerID0", t0."EmployeeID", t0."OrderDate", t."CustomerID" AS "CustomerID1" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c CROSS JOIN ( - SELECT c1."CustomerID" - FROM "Customers" AS c1 - WHERE c1."City" LIKE 'S%' - ) AS t + SELECT 1 + FROM "Customers" AS c0 + WHERE c0."City" LIKE 'S%' + ) AS c1 JOIN LATERAL ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT 1 FROM "Orders" AS o - WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c0."ContactName")::int - ) AS t0 ON TRUE - WHERE c0."CustomerID" LIKE 'F%' -) AS t1 -WHERE c."CustomerID" = t1."CustomerID" + WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c."ContactName")::int + ) AS o0 ON TRUE + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c2."CustomerID" = s."CustomerID" """); } @@ -1322,24 +1318,24 @@ public override async Task Update_with_cross_join_outer_apply_set_constant(bool AssertExecuteUpdateSql( """ -UPDATE "Customers" AS c +UPDATE "Customers" AS c2 SET "ContactName" = 'Updated' FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region", t."CustomerID" AS "CustomerID0", t."Address" AS "Address0", t."City" AS "City0", t."CompanyName" AS "CompanyName0", t."ContactName" AS "ContactName0", t."ContactTitle" AS "ContactTitle0", t."Country" AS "Country0", t."Fax" AS "Fax0", t."Phone" AS "Phone0", t."PostalCode" AS "PostalCode0", t."Region" AS "Region0", t0."OrderID", t0."CustomerID" AS "CustomerID1", t0."EmployeeID", t0."OrderDate" - FROM "Customers" AS c0 + SELECT c."CustomerID" + FROM "Customers" AS c CROSS JOIN ( - SELECT c1."CustomerID", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region" - FROM "Customers" AS c1 - WHERE c1."City" LIKE 'S%' - ) AS t + SELECT 1 + FROM "Customers" AS c0 + WHERE c0."City" LIKE 'S%' + ) AS c1 LEFT JOIN LATERAL ( - SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" + SELECT 1 FROM "Orders" AS o - WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c0."ContactName")::int - ) AS t0 ON TRUE - WHERE c0."CustomerID" LIKE 'F%' -) AS t1 -WHERE c."CustomerID" = t1."CustomerID" + WHERE o."OrderID" < 10300 AND date_part('year', o."OrderDate")::int < length(c."ContactName")::int + ) AS o0 ON TRUE + WHERE c."CustomerID" LIKE 'F%' +) AS s +WHERE c2."CustomerID" = s."CustomerID" """); } @@ -1356,19 +1352,19 @@ public override async Task Update_Where_SelectMany_subquery_set_null(bool async) AssertExecuteUpdateSql( """ -UPDATE "Orders" AS o +UPDATE "Orders" AS o1 SET "OrderDate" = NULL FROM ( - SELECT t."OrderID", t."CustomerID", t."EmployeeID", t."OrderDate", c."CustomerID" AS "CustomerID0" + SELECT o0."OrderID" FROM "Customers" AS c INNER JOIN ( - SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" - FROM "Orders" AS o0 - WHERE date_part('year', o0."OrderDate")::int = 1997 - ) AS t ON c."CustomerID" = t."CustomerID" + SELECT o."OrderID", o."CustomerID" + FROM "Orders" AS o + WHERE date_part('year', o."OrderDate")::int = 1997 + ) AS o0 ON c."CustomerID" = o0."CustomerID" WHERE c."CustomerID" LIKE 'F%' -) AS t0 -WHERE o."OrderID" = t0."OrderID" +) AS s +WHERE o1."OrderID" = s."OrderID" """); } @@ -1379,12 +1375,12 @@ public override async Task Update_Where_Join_set_property_from_joined_single_res AssertExecuteUpdateSql( """ UPDATE "Customers" AS c -SET "City" = date_part('year', ( +SET "City" = COALESCE(date_part('year', ( SELECT o."OrderDate" FROM "Orders" AS o WHERE c."CustomerID" = o."CustomerID" ORDER BY o."OrderDate" DESC NULLS LAST - LIMIT 1))::int::text + LIMIT 1))::int::text, '') WHERE c."CustomerID" LIKE 'F%' """); } @@ -1396,12 +1392,12 @@ public override async Task Update_Where_Join_set_property_from_joined_table(bool AssertExecuteUpdateSql( """ UPDATE "Customers" AS c -SET "City" = t."City" +SET "City" = c1."City" FROM ( - SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" + SELECT c0."City" FROM "Customers" AS c0 WHERE c0."CustomerID" = 'ALFKI' -) AS t +) AS c1 WHERE c."CustomerID" LIKE 'F%' """); } @@ -1413,12 +1409,12 @@ public override async Task Update_Where_Join_set_property_from_joined_single_res AssertExecuteUpdateSql( """ UPDATE "Customers" AS c -SET "City" = date_part('year', ( +SET "City" = COALESCE(date_part('year', ( SELECT o."OrderDate" FROM "Orders" AS o WHERE c."CustomerID" = o."CustomerID" ORDER BY o."OrderDate" DESC NULLS LAST - LIMIT 1))::int::text + LIMIT 1))::int::text, '') WHERE c."CustomerID" LIKE 'F%' """); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs index 1d19a430f..ae128131e 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,16 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPCFiltersInheritanceBulkUpdatesNpgsqlTest - : TPCFiltersInheritanceBulkUpdatesTestBase +public class TPCFiltersInheritanceBulkUpdatesNpgsqlTest( + TPCFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : TPCFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { - public TPCFiltersInheritanceBulkUpdatesNpgsqlTest( - TPCFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); @@ -40,13 +35,13 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM ( - SELECT e."Id", e."CountryId", e."Name", e."Species", e."EagleId", e."IsFlightless", e."Group", NULL AS "FoundOn", 'Eagle' AS "Discriminator" + SELECT e."CountryId" FROM "Eagle" AS e UNION ALL - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE t."CountryId" = 1 AND c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE u."CountryId" = 1 AND c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -60,10 +55,10 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM ( - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE t."CountryId" = 1 AND c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE u."CountryId" = 1 AND c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -158,13 +153,13 @@ public override async Task Update_where_using_hierarchy(bool async) WHERE ( SELECT count(*)::int FROM ( - SELECT e."Id", e."CountryId", e."Name", e."Species", e."EagleId", e."IsFlightless", e."Group", NULL AS "FoundOn", 'Eagle' AS "Discriminator" + SELECT e."CountryId" FROM "Eagle" AS e UNION ALL - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE t."CountryId" = 1 AND c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE u."CountryId" = 1 AND c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -192,10 +187,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) WHERE ( SELECT count(*)::int FROM ( - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE t."CountryId" = 1 AND c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE u."CountryId" = 1 AND c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs index 90f2a52d9..f16b4aad5 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,16 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPCInheritanceBulkUpdatesNpgsqlTest - : TPCInheritanceBulkUpdatesTestBase +public class TPCInheritanceBulkUpdatesNpgsqlTest( + TPCInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : TPCInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { - public TPCInheritanceBulkUpdatesNpgsqlTest( - TPCInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); @@ -40,13 +35,13 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM ( - SELECT e."Id", e."CountryId", e."Name", e."Species", e."EagleId", e."IsFlightless", e."Group", NULL AS "FoundOn", 'Eagle' AS "Discriminator" + SELECT e."CountryId" FROM "Eagle" AS e UNION ALL - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -60,10 +55,10 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM ( - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -156,13 +151,13 @@ public override async Task Update_where_using_hierarchy(bool async) WHERE ( SELECT count(*)::int FROM ( - SELECT e."Id", e."CountryId", e."Name", e."Species", e."EagleId", e."IsFlightless", e."Group", NULL AS "FoundOn", 'Eagle' AS "Discriminator" + SELECT e."CountryId" FROM "Eagle" AS e UNION ALL - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } @@ -189,10 +184,10 @@ public override async Task Update_where_using_hierarchy_derived(bool async) WHERE ( SELECT count(*)::int FROM ( - SELECT k."Id", k."CountryId", k."Name", k."Species", k."EagleId", k."IsFlightless", NULL AS "Group", k."FoundOn", 'Kiwi' AS "Discriminator" + SELECT k."CountryId" FROM "Kiwi" AS k - ) AS t - WHERE c."Id" = t."CountryId" AND t."CountryId" > 0) > 0 + ) AS u + WHERE c."Id" = u."CountryId" AND u."CountryId" > 0) > 0 """); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs index fcf00633d..4b8c44424 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,18 +2,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPHFiltersInheritanceBulkUpdatesNpgsqlTest : FiltersInheritanceBulkUpdatesTestBase< - TPHFiltersInheritanceBulkUpdatesNpgsqlFixture> +public class TPHFiltersInheritanceBulkUpdatesNpgsqlTest( + TPHFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : FiltersInheritanceBulkUpdatesRelationalTestBase< + TPHFiltersInheritanceBulkUpdatesNpgsqlFixture>(fixture, testOutputHelper) { - public TPHFiltersInheritanceBulkUpdatesNpgsqlTest( - TPHFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs index dd076dd8f..dee6e67aa 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs @@ -13,6 +13,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity().HasNoKey().ToSqlQuery(@"SELECT * FROM ""Animals"""); + modelBuilder.Entity().HasNoKey().ToSqlQuery(""" + SELECT * FROM "Animals" + """); } } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs index 3fafaa0a8..21bb652f8 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,17 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPHInheritanceBulkUpdatesNpgsqlTest : TPHInheritanceBulkUpdatesTestBase +public class TPHInheritanceBulkUpdatesNpgsqlTest( + TPHInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : TPHInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { - public TPHInheritanceBulkUpdatesNpgsqlTest( - TPHInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs index b0590845f..6ecf13d06 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,16 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPTFiltersInheritanceBulkUpdatesSqlServerTest - : TPTFiltersInheritanceBulkUpdatesTestBase +public class TPTFiltersInheritanceBulkUpdatesSqlServerTest( + TPTFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : TPTFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { - public TPTFiltersInheritanceBulkUpdatesSqlServerTest( - TPTFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); @@ -36,9 +31,6 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" - LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE a."CountryId" = 1 AND c."Id" = a."CountryId" AND a."CountryId" > 0) > 0 """); } @@ -53,8 +45,6 @@ DELETE FROM "Countries" AS c WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE a."CountryId" = 1 AND c."Id" = a."CountryId" AND k."Id" IS NOT NULL AND a."CountryId" > 0) > 0 """); @@ -88,20 +78,14 @@ public override async Task Update_base_type(bool async) // TODO: This over-complex SQL would get pruned after https://github.com/dotnet/efcore/issues/31083 AssertExecuteUpdateSql( """ -UPDATE "Animals" AS a +UPDATE "Animals" AS a0 SET "Name" = 'Animal' FROM ( - SELECT a0."Id", a0."CountryId", a0."Name", a0."Species", b0."EagleId", b0."IsFlightless", e0."Group", k0."FoundOn", CASE - WHEN k0."Id" IS NOT NULL THEN 'Kiwi' - WHEN e0."Id" IS NOT NULL THEN 'Eagle' - END AS "Discriminator" - FROM "Animals" AS a0 - LEFT JOIN "Birds" AS b0 ON a0."Id" = b0."Id" - LEFT JOIN "Eagle" AS e0 ON a0."Id" = e0."Id" - LEFT JOIN "Kiwi" AS k0 ON a0."Id" = k0."Id" - WHERE a0."CountryId" = 1 AND a0."Name" = 'Great spotted kiwi' -) AS t -WHERE a."Id" = t."Id" + SELECT a."Id" + FROM "Animals" AS a + WHERE a."CountryId" = 1 AND a."Name" = 'Great spotted kiwi' +) AS s +WHERE a0."Id" = s."Id" """); } @@ -112,20 +96,17 @@ public override async Task Update_base_type_with_OfType(bool async) // TODO: This over-complex SQL would get pruned after https://github.com/dotnet/efcore/issues/31083 AssertExecuteUpdateSql( """ -UPDATE "Animals" AS a +UPDATE "Animals" AS a0 SET "Name" = 'NewBird' FROM "Birds" AS b, - "Kiwi" AS k, + "Kiwi" AS k0, ( - SELECT a0."Id", a0."CountryId", a0."Name", a0."Species", b0."EagleId", b0."IsFlightless", k0."FoundOn", CASE - WHEN k0."Id" IS NOT NULL THEN 'Kiwi' - END AS "Discriminator" - FROM "Animals" AS a0 - LEFT JOIN "Birds" AS b0 ON a0."Id" = b0."Id" - LEFT JOIN "Kiwi" AS k0 ON a0."Id" = k0."Id" - WHERE a0."CountryId" = 1 AND k0."Id" IS NOT NULL - ) AS t -WHERE a."Id" = t."Id" AND a."Id" = k."Id" AND a."Id" = b."Id" + SELECT a."Id" + FROM "Animals" AS a + LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" + WHERE a."CountryId" = 1 AND k."Id" IS NOT NULL + ) AS s +WHERE a0."Id" = s."Id" AND a0."Id" = k0."Id" AND a0."Id" = b."Id" """); } @@ -196,9 +177,6 @@ public override async Task Update_where_using_hierarchy(bool async) WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" - LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE a."CountryId" = 1 AND c."Id" = a."CountryId" AND a."CountryId" > 0) > 0 """); } @@ -214,8 +192,6 @@ public override async Task Update_where_using_hierarchy_derived(bool async) WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE a."CountryId" = 1 AND c."Id" = a."CountryId" AND k."Id" IS NOT NULL AND a."CountryId" > 0) > 0 """); diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs index 503c0ae55..9516e6ec7 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs @@ -2,15 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; -public class TPTInheritanceBulkUpdatesNpgsqlTest : TPTInheritanceBulkUpdatesTestBase +public class TPTInheritanceBulkUpdatesNpgsqlTest( + TPTInheritanceBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : TPTInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { - public TPTInheritanceBulkUpdatesNpgsqlTest( - TPTInheritanceBulkUpdatesNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - public override async Task Delete_where_hierarchy(bool async) { await base.Delete_where_hierarchy(async); @@ -67,20 +63,14 @@ public override async Task Update_base_type(bool async) // TODO: This over-complex SQL would get pruned after https://github.com/dotnet/efcore/issues/31083 AssertExecuteUpdateSql( """ -UPDATE "Animals" AS a +UPDATE "Animals" AS a0 SET "Name" = 'Animal' FROM ( - SELECT a0."Id", a0."CountryId", a0."Name", a0."Species", b0."EagleId", b0."IsFlightless", e0."Group", k0."FoundOn", CASE - WHEN k0."Id" IS NOT NULL THEN 'Kiwi' - WHEN e0."Id" IS NOT NULL THEN 'Eagle' - END AS "Discriminator" - FROM "Animals" AS a0 - LEFT JOIN "Birds" AS b0 ON a0."Id" = b0."Id" - LEFT JOIN "Eagle" AS e0 ON a0."Id" = e0."Id" - LEFT JOIN "Kiwi" AS k0 ON a0."Id" = k0."Id" - WHERE a0."Name" = 'Great spotted kiwi' -) AS t -WHERE a."Id" = t."Id" + SELECT a."Id" + FROM "Animals" AS a + WHERE a."Name" = 'Great spotted kiwi' +) AS s +WHERE a0."Id" = s."Id" """); } @@ -91,20 +81,17 @@ public override async Task Update_base_type_with_OfType(bool async) // TODO: This over-complex SQL would get pruned after https://github.com/dotnet/efcore/issues/31083 AssertExecuteUpdateSql( """ -UPDATE "Animals" AS a +UPDATE "Animals" AS a0 SET "Name" = 'NewBird' FROM "Birds" AS b, - "Kiwi" AS k, + "Kiwi" AS k0, ( - SELECT a0."Id", a0."CountryId", a0."Name", a0."Species", b0."EagleId", b0."IsFlightless", k0."FoundOn", CASE - WHEN k0."Id" IS NOT NULL THEN 'Kiwi' - END AS "Discriminator" - FROM "Animals" AS a0 - LEFT JOIN "Birds" AS b0 ON a0."Id" = b0."Id" - LEFT JOIN "Kiwi" AS k0 ON a0."Id" = k0."Id" - WHERE k0."Id" IS NOT NULL - ) AS t -WHERE a."Id" = t."Id" AND a."Id" = k."Id" AND a."Id" = b."Id" + SELECT a."Id" + FROM "Animals" AS a + LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" + WHERE k."Id" IS NOT NULL + ) AS s +WHERE a0."Id" = s."Id" AND a0."Id" = k0."Id" AND a0."Id" = b."Id" """); } @@ -175,9 +162,6 @@ public override async Task Update_where_using_hierarchy(bool async) WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" - LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE c."Id" = a."CountryId" AND a."CountryId" > 0) > 0 """); } @@ -193,8 +177,6 @@ public override async Task Update_where_using_hierarchy_derived(bool async) WHERE ( SELECT count(*)::int FROM "Animals" AS a - LEFT JOIN "Birds" AS b ON a."Id" = b."Id" - LEFT JOIN "Eagle" AS e ON a."Id" = e."Id" LEFT JOIN "Kiwi" AS k ON a."Id" = k."Id" WHERE c."Id" = a."CountryId" AND k."Id" IS NOT NULL AND a."CountryId" > 0) > 0 """); diff --git a/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs index ced8872ae..26799628e 100644 --- a/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public abstract class CommandInterceptionNpgsqlTestBase : CommandInterceptionTestBase +public abstract class CommandInterceptionNpgsqlTestBase(CommandInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) + : CommandInterceptionTestBase(fixture) { - public CommandInterceptionNpgsqlTestBase(InterceptionNpgsqlFixtureBase fixture) - : base(fixture) - { - } - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase { protected override string StoreName @@ -25,14 +21,9 @@ protected override IServiceCollection InjectInterceptors( => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); } - public class CommandInterceptionNpgsqlTest - : CommandInterceptionNpgsqlTestBase, IClassFixture + public class CommandInterceptionNpgsqlTest(CommandInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) + : CommandInterceptionNpgsqlTestBase(fixture), IClassFixture { - public CommandInterceptionNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener @@ -47,14 +38,10 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build } } - public class CommandInterceptionWithDiagnosticsNpgsqlTest - : CommandInterceptionNpgsqlTestBase, IClassFixture + public class CommandInterceptionWithDiagnosticsNpgsqlTest( + CommandInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) + : CommandInterceptionNpgsqlTestBase(fixture), IClassFixture { - public CommandInterceptionWithDiagnosticsNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener diff --git a/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs b/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs index 6e66276b3..3770aaf73 100644 --- a/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs +++ b/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class CompositeKeyEndToEndNpgsqlTest : CompositeKeyEndToEndTestBase +public class CompositeKeyEndToEndNpgsqlTest(CompositeKeyEndToEndNpgsqlTest.CompositeKeyEndToEndNpgsqlFixture fixture) + : CompositeKeyEndToEndTestBase(fixture) { - public CompositeKeyEndToEndNpgsqlTest(CompositeKeyEndToEndNpgsqlFixture fixture) - : base(fixture) - { - } - public class CompositeKeyEndToEndNpgsqlFixture : CompositeKeyEndToEndFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs b/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs index 4565752b1..5c3c86cab 100644 --- a/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs +++ b/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs @@ -3,7 +3,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; [MinimumPostgresVersion(12, 0)] -public class ComputedColumnTest : IDisposable +public class ComputedColumnTest : IAsyncLifetime { [ConditionalFact] public void Can_use_computed_columns() @@ -47,16 +47,10 @@ public void Can_use_computed_columns_with_null_values() Assert.Null(entity.P5); } - private class Context : DbContext + private class Context(IServiceProvider serviceProvider, string databaseName) : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly string _databaseName; - - public Context(IServiceProvider serviceProvider, string databaseName) - { - _serviceProvider = serviceProvider; - _databaseName = databaseName; - } + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly string _databaseName = databaseName; public DbSet Entities { get; set; } @@ -69,11 +63,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.P4) - .HasComputedColumnSql(@"""P1"" + ""P2""", stored: true); + .HasComputedColumnSql(""" + "P1" + "P2" + """, stored: true); modelBuilder.Entity() .Property(e => e.P5) - .HasComputedColumnSql(@"""P1"" + ""P3""", stored: true); + .HasComputedColumnSql(""" + "P1" + "P3" + """, stored: true); } } @@ -103,28 +101,21 @@ public class EnumItem public FlagEnum? CalculatedFlagEnum { get; set; } } - private class NullableContext : DbContext + private class NullableContext(IServiceProvider serviceProvider, string databaseName) : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly string _databaseName; - - public NullableContext(IServiceProvider serviceProvider, string databaseName) - { - _serviceProvider = serviceProvider; - _databaseName = databaseName; - } - public DbSet EnumItems { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()) - .UseInternalServiceProvider(_serviceProvider); + .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()) + .UseInternalServiceProvider(serviceProvider); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity() .Property(entity => entity.CalculatedFlagEnum) - .HasComputedColumnSql(@"""FlagEnum"" | ""OptionalFlagEnum""", stored: true); + .HasComputedColumnSql(""" + "FlagEnum" | "OptionalFlagEnum" + """, stored: true); } [ConditionalFact] @@ -143,13 +134,11 @@ public void Can_use_computed_columns_with_nullable_enum() Assert.Equal(FlagEnum.AValue | FlagEnum.BValue, entity.CalculatedFlagEnum); } - public ComputedColumnTest() - { - TestStore = NpgsqlTestStore.CreateInitialized("ComputedColumnTest"); - } + protected NpgsqlTestStore TestStore { get; private set; } - protected NpgsqlTestStore TestStore { get; } + public async Task InitializeAsync() + => TestStore = await NpgsqlTestStore.CreateInitializedAsync("ComputedColumnTest"); - public virtual void Dispose() - => TestStore.Dispose(); + public async Task DisposeAsync() + => await TestStore.DisposeAsync(); } diff --git a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs index d8c24046a..d31212647 100644 --- a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs @@ -14,8 +14,12 @@ public ConcurrencyDetectorDisabledNpgsqlTest(ConcurrencyDetectorNpgsqlFixture fi public override Task FromSql(bool async) => ConcurrencyDetectorTest( async c => async - ? await c.Products.FromSqlRaw(@"select * from ""Products""").ToListAsync() - : c.Products.FromSqlRaw(@"select * from ""Products""").ToList()); + ? await c.Products.FromSqlRaw(""" + select * from "Products" + """).ToListAsync() + : c.Products.FromSqlRaw(""" + select * from "Products" + """).ToList()); protected override async Task ConcurrencyDetectorTest(Func> test) { diff --git a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs index 8a0f4d5ff..6cb940edd 100644 --- a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs @@ -14,8 +14,12 @@ public ConcurrencyDetectorEnabledNpgsqlTest(ConcurrencyDetectorNpgsqlFixture fix public override Task FromSql(bool async) => ConcurrencyDetectorTest( async c => async - ? await c.Products.FromSqlRaw(@"select * from ""Products""").ToListAsync() - : c.Products.FromSqlRaw(@"select * from ""Products""").ToList()); + ? await c.Products.FromSqlRaw(""" + select * from "Products" + """).ToListAsync() + : c.Products.FromSqlRaw(""" + select * from "Products" + """).ToList()); protected override async Task ConcurrencyDetectorTest(Func> test) { diff --git a/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs index 4a9611f0a..fbfe6f2c2 100644 --- a/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class ConferencePlannerNpgsqlTest : ConferencePlannerTestBase +public class ConferencePlannerNpgsqlTest(ConferencePlannerNpgsqlTest.ConferencePlannerNpgsqlFixture fixture) + : ConferencePlannerTestBase(fixture) { - public ConferencePlannerNpgsqlTest(ConferencePlannerNpgsqlFixture fixture) - : base(fixture) - { - } - // Overridden to use UTC DateTimeOffsets public override async Task SessionsController_Post() => await ExecuteWithStrategyInTransactionAsync( @@ -49,7 +45,7 @@ protected override ITestStoreFactory TestStoreFactory // We don't support DateTimeOffset with non-zero offsets, so we unfortunately need to override the entire seeding method. // See https://github.com/dotnet/efcore/issues/26068 - protected override void Seed(ApplicationDbContext context) + protected override async Task SeedAsync(ApplicationDbContext context) { var attendees1 = new List { @@ -166,7 +162,7 @@ protected override void Seed(ApplicationDbContext context) } context.AddRange(tracks.Values); - context.SaveChanges(); + await context.SaveChangesAsync(); } } } diff --git a/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs index e8c47ed71..2d8f44e13 100644 --- a/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public abstract class ConnectionInterceptionNpgsqlTestBase : ConnectionInterceptionTestBase +public abstract class ConnectionInterceptionNpgsqlTestBase(ConnectionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) + : ConnectionInterceptionTestBase(fixture) { - protected ConnectionInterceptionNpgsqlTestBase(InterceptionNpgsqlFixtureBase fixture) - : base(fixture) - { - } - [ConditionalTheory(Skip = "#2368")] public override Task Intercept_connection_creation_passively(bool async) => base.Intercept_connection_creation_passively(async); @@ -79,14 +75,9 @@ protected override DbCommand CreateDbCommand() => throw new NotImplementedException(); } - public class ConnectionInterceptionNpgsqlTest - : ConnectionInterceptionNpgsqlTestBase, IClassFixture + public class ConnectionInterceptionNpgsqlTest(ConnectionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) + : ConnectionInterceptionNpgsqlTestBase(fixture), IClassFixture { - public ConnectionInterceptionNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener @@ -94,14 +85,11 @@ protected override bool ShouldSubscribeToDiagnosticListener } } - public class ConnectionInterceptionWithDiagnosticsNpgsqlTest - : ConnectionInterceptionNpgsqlTestBase, IClassFixture + public class ConnectionInterceptionWithDiagnosticsNpgsqlTest( + ConnectionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) + : ConnectionInterceptionNpgsqlTestBase(fixture), + IClassFixture { - public ConnectionInterceptionWithDiagnosticsNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener diff --git a/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs b/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs index 539d70731..2cac8e343 100644 --- a/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs @@ -7,25 +7,25 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class ConnectionSpecificationTest { [Fact] - public void Can_specify_connection_string_in_OnConfiguring() + public async Task Can_specify_connection_string_in_OnConfiguring() { var serviceProvider = new ServiceCollection() .AddDbContext() .BuildServiceProvider(); - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = serviceProvider.GetRequiredService(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } [Fact] - public void Can_specify_connection_string_in_OnConfiguring_with_default_service_provider() + public async Task Can_specify_connection_string_in_OnConfiguring_with_default_service_provider() { - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = new StringInOnConfiguringContext(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = new StringInOnConfiguringContext(); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } private class StringInOnConfiguringContext : NorthwindContextBase @@ -35,42 +35,35 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } [Fact] - public void Can_specify_connection_in_OnConfiguring() + public async Task Can_specify_connection_in_OnConfiguring() { var serviceProvider = new ServiceCollection() .AddScoped(_ => new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)) .AddDbContext().BuildServiceProvider(); - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = serviceProvider.GetRequiredService(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } [Fact] - public void Can_specify_connection_in_OnConfiguring_with_default_service_provider() + public async Task Can_specify_connection_in_OnConfiguring_with_default_service_provider() { - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = new ConnectionInOnConfiguringContext(new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = new ConnectionInOnConfiguringContext(new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } - private class ConnectionInOnConfiguringContext : NorthwindContextBase + private class ConnectionInOnConfiguringContext(NpgsqlConnection connection) : NorthwindContextBase { - private readonly NpgsqlConnection _connection; - - public ConnectionInOnConfiguringContext(NpgsqlConnection connection) - { - _connection = connection; - } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(_connection, b => b.ApplyConfiguration()); + => optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); public override void Dispose() { - _connection.Dispose(); + connection.Dispose(); base.Dispose(); } } @@ -108,94 +101,77 @@ public void Throws_if_no_config_without_UseNpgsql() } // ReSharper disable once ClassNeverInstantiated.Local - private class NoUseNpgsqlContext : NorthwindContextBase - { - } + private class NoUseNpgsqlContext : NorthwindContextBase; [Fact] - public void Can_depend_on_DbContextOptions() + public async Task Can_depend_on_DbContextOptions() { var serviceProvider = new ServiceCollection() .AddScoped(_ => new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)) .AddDbContext() .BuildServiceProvider(); - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = serviceProvider.GetRequiredService(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } [Fact] - public void Can_depend_on_DbContextOptions_with_default_service_provider() + public async Task Can_depend_on_DbContextOptions_with_default_service_provider() { - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = new OptionsContext( + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = new OptionsContext( new DbContextOptions(), new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } - private class OptionsContext : NorthwindContextBase + private class OptionsContext(DbContextOptions options, NpgsqlConnection connection) + : NorthwindContextBase(options) { - private readonly NpgsqlConnection _connection; - private readonly DbContextOptions _options; - - public OptionsContext(DbContextOptions options, NpgsqlConnection connection) - : base(options) - { - _options = options; - _connection = connection; - } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - Assert.Same(_options, optionsBuilder.Options); + Assert.Same(options, optionsBuilder.Options); - optionsBuilder.UseNpgsql(_connection, b => b.ApplyConfiguration()); + optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); - Assert.NotSame(_options, optionsBuilder.Options); + Assert.NotSame(options, optionsBuilder.Options); } public override void Dispose() { - _connection.Dispose(); + connection.Dispose(); base.Dispose(); } } [Fact] - public void Can_depend_on_non_generic_options_when_only_one_context() + public async Task Can_depend_on_non_generic_options_when_only_one_context() { var serviceProvider = new ServiceCollection() .AddDbContext() .BuildServiceProvider(); - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = serviceProvider.GetRequiredService(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } [Fact] - public void Can_depend_on_non_generic_options_when_only_one_context_with_default_service_provider() + public async Task Can_depend_on_non_generic_options_when_only_one_context_with_default_service_provider() { - using var _ = NpgsqlTestStore.GetNorthwindStore(); - using var context = new NonGenericOptionsContext(new DbContextOptions()); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); + await using var context = new NonGenericOptionsContext(new DbContextOptions()); - Assert.True(context.Customers.Any()); + Assert.True(await context.Customers.AnyAsync()); } - private class NonGenericOptionsContext : NorthwindContextBase + private class NonGenericOptionsContext(DbContextOptions options) : NorthwindContextBase(options) { - private readonly DbContextOptions _options; - - public NonGenericOptionsContext(DbContextOptions options) - : base(options) - { - _options = options; - } + private readonly DbContextOptions _options = options; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -246,68 +222,62 @@ private class Customer #region Added for Npgsql [Fact] - public void Can_create_admin_connection_with_data_source() + public async Task Can_create_admin_connection_with_data_source() { - using var dataSource = NpgsqlDataSource.Create(NpgsqlTestStore.NorthwindConnectionString); + await using var dataSource = NpgsqlDataSource.Create(NpgsqlTestStore.NorthwindConnectionString); - using var _ = NpgsqlTestStore.GetNorthwindStore(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql(dataSource, b => b.ApplyConfiguration()); - using var context = new GeneralOptionsContext(optionsBuilder.Options); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); var relationalConnection = context.GetService(); - using var adminConnection = relationalConnection.CreateAdminConnection(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); - adminConnection.Open(); + await adminConnection.OpenAsync(CancellationToken.None); } [Fact] - public void Can_create_admin_connection_with_connection_string() + public async Task Can_create_admin_connection_with_connection_string() { - using var _ = NpgsqlTestStore.GetNorthwindStore(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql(NpgsqlTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); - using var context = new GeneralOptionsContext(optionsBuilder.Options); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); var relationalConnection = context.GetService(); - using var adminConnection = relationalConnection.CreateAdminConnection(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); - adminConnection.Open(); + await adminConnection.OpenAsync(CancellationToken.None); } [Fact] - public void Can_create_admin_connection_with_connection() + public async Task Can_create_admin_connection_with_connection() { - using var connection = new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString); + await using var connection = new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString); connection.Open(); - using var _ = NpgsqlTestStore.GetNorthwindStore(); + await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); - using var context = new GeneralOptionsContext(optionsBuilder.Options); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); var relationalConnection = context.GetService(); - using var adminConnection = relationalConnection.CreateAdminConnection(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); adminConnection.Open(); } - private class GeneralOptionsContext : NorthwindContextBase - { - public GeneralOptionsContext(DbContextOptions options) - : base(options) - { - } - } + private class GeneralOptionsContext(DbContextOptions options) : NorthwindContextBase(options); #endregion } diff --git a/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs index 8c7a1d96b..50fd6c5ee 100644 --- a/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs @@ -12,53 +12,56 @@ public ConvertToProviderTypesNpgsqlTest(ConvertToProviderTypesNpgsqlFixture fixt Fixture.TestSqlLoggerFactory.Clear(); } - [Fact] - public override void Can_insert_and_read_with_max_length_set() - { - const string shortString = "Sky"; - var shortBinary = new byte[] { 8, 8, 7, 8, 7 }; - - var longString = new string('X', Fixture.LongStringLength); - var longBinary = new byte[Fixture.LongStringLength]; - for (var i = 0; i < longBinary.Length; i++) - { - longBinary[i] = (byte)i; - } - - using (var context = CreateContext()) - { - context.Set().Add( - new MaxLengthDataTypes - { - Id = 79, - String3 = shortString, - ByteArray5 = shortBinary, - String9000 = longString, - ByteArray9000 = longBinary - }); - - Assert.Equal(1, context.SaveChanges()); - } - - using (var context = CreateContext()) - { - var dt = context.Set().Where(e => e.Id == 79).ToList().Single(); - - Assert.Equal(shortString, dt.String3); - Assert.Equal(shortBinary, dt.ByteArray5); - Assert.Equal(longString, dt.String9000); - Assert.Equal(longBinary, dt.ByteArray9000); - } - } + // [Fact] + // public override void Can_insert_and_read_with_max_length_set() + // { + // const string shortString = "Sky"; + // var shortBinary = new byte[] { 8, 8, 7, 8, 7 }; + // + // var longString = new string('X', Fixture.LongStringLength); + // var longBinary = new byte[Fixture.LongStringLength]; + // for (var i = 0; i < longBinary.Length; i++) + // { + // longBinary[i] = (byte)i; + // } + // + // using (var context = CreateContext()) + // { + // context.Set().Add( + // new MaxLengthDataTypes + // { + // Id = 79, + // String3 = shortString, + // ByteArray5 = shortBinary, + // String9000 = longString, + // ByteArray9000 = longBinary + // }); + // + // Assert.Equal(1, context.SaveChanges()); + // } + // + // using (var context = CreateContext()) + // { + // var dt = context.Set().Where(e => e.Id == 79).ToList().Single(); + // + // Assert.Equal(shortString, dt.String3); + // Assert.Equal(shortBinary, dt.ByteArray5); + // Assert.Equal(longString, dt.String9000); + // Assert.Equal(longBinary, dt.ByteArray9000); + // } + // } [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_non_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_non_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_object_backed_data_types() { } + public override Task Can_insert_and_read_back_object_backed_data_types() + => Task.CompletedTask; public class ConvertToProviderTypesNpgsqlFixture : ConvertToProviderTypesFixtureBase { diff --git a/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs index 085f560c4..a08007fe7 100644 --- a/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs @@ -2,27 +2,28 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class CustomConvertersNpgsqlTest : CustomConvertersTestBase +public class CustomConvertersNpgsqlTest(CustomConvertersNpgsqlTest.CustomConvertersNpgsqlFixture fixture) + : CustomConvertersTestBase(fixture) { - public CustomConvertersNpgsqlTest(CustomConvertersNpgsqlFixture fixture) - : base(fixture) - { - } - // Disabled: PostgreSQL is case-sensitive - public override void Can_insert_and_read_back_with_case_insensitive_string_key() { } + public override Task Can_insert_and_read_back_with_case_insensitive_string_key() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_non_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_non_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_nullable_backed_data_types() { } + public override Task Can_insert_and_read_back_nullable_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_object_backed_data_types() { } + public override Task Can_insert_and_read_back_object_backed_data_types() + => Task.CompletedTask; [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_query_using_any_data_type_nullable_shadow() { } + public override Task Can_query_using_any_data_type_nullable_shadow() + => Task.CompletedTask; public override void Value_conversion_on_enum_collection_contains() => Assert.Contains( diff --git a/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs index cc2b9f362..71b56bd3a 100644 --- a/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs @@ -2,33 +2,23 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class DataAnnotationNpgsqlTest : DataAnnotationRelationalTestBase +public class DataAnnotationNpgsqlTest(DataAnnotationNpgsqlTest.DataAnnotationNpgsqlFixture fixture) + : DataAnnotationRelationalTestBase(fixture) { - public DataAnnotationNpgsqlTest(DataAnnotationNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); protected override TestHelpers TestHelpers => NpgsqlTestHelpers.Instance; - public override void StringLengthAttribute_throws_while_inserting_value_longer_than_max_length() - { - // Npgsql does not support length - } + public override Task StringLengthAttribute_throws_while_inserting_value_longer_than_max_length() + => Task.CompletedTask; // Npgsql does not support length - public override void TimestampAttribute_throws_if_value_in_database_changed() - { - // Npgsql does not support length - } + public override Task TimestampAttribute_throws_if_value_in_database_changed() + => Task.CompletedTask; // Npgsql does not support length - public override void MaxLengthAttribute_throws_while_inserting_value_longer_than_max_length() - { - // Npgsql does not support length - } + public override Task MaxLengthAttribute_throws_while_inserting_value_longer_than_max_length() + => Task.CompletedTask; // Npgsql does not support length public class DataAnnotationNpgsqlFixture : DataAnnotationRelationalFixtureBase { diff --git a/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs index 308ef08e7..9e53df97a 100644 --- a/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs @@ -1,9 +1,3 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class DataBindingNpgsqlTest : DataBindingTestBase -{ - public DataBindingNpgsqlTest(F1BytesNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class DataBindingNpgsqlTest(F1BytesNpgsqlFixture fixture) : DataBindingTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs b/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs index b3677ed03..611655a01 100644 --- a/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs +++ b/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs @@ -43,16 +43,10 @@ public void Dispose() context.Database.EnsureDeleted(); } - private class ChipsContext : DbContext + private class ChipsContext(IServiceProvider serviceProvider, string databaseName) : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly string _databaseName; - - public ChipsContext(IServiceProvider serviceProvider, string databaseName) - { - _serviceProvider = serviceProvider; - _databaseName = databaseName; - } + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly string _databaseName = databaseName; // ReSharper disable once UnusedAutoPropertyAccessor.Local public DbSet Chips { get; set; } diff --git a/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs index 88e113ac7..3bfffa890 100644 --- a/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs @@ -3,13 +3,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class DesignTimeNpgsqlTest : DesignTimeTestBase +public class DesignTimeNpgsqlTest(DesignTimeNpgsqlTest.DesignTimeNpgsqlFixture fixture) + : DesignTimeTestBase(fixture) { - public DesignTimeNpgsqlTest(DesignTimeNpgsqlFixture fixture) - : base(fixture) - { - } - protected override Assembly ProviderAssembly => typeof(NpgsqlDesignTimeServices).Assembly; diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index d842ad664..ce5bd43a8 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -9,6 +9,7 @@ + @@ -17,6 +18,10 @@ + + + + diff --git a/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs index dccba770b..8552d88f9 100644 --- a/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs @@ -2,13 +2,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class EntitySplittingNpgsqlTest : EntitySplittingTestBase +public class EntitySplittingNpgsqlTest(ITestOutputHelper testOutputHelper) : EntitySplittingTestBase(testOutputHelper) { - public EntitySplittingNpgsqlTest(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; } diff --git a/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs b/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs index 1d82ef469..f488081c6 100644 --- a/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs +++ b/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs @@ -80,7 +80,7 @@ private void Test_commit_failure(bool realFailure, Action(); - connection.CommitFailures.Enqueue(new bool?[] { realFailure }); + connection.CommitFailures.Enqueue([realFailure]); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); @@ -99,7 +99,7 @@ private void Test_commit_failure(bool realFailure, Action l.Id == CoreEventId.ExecutionStrategyRetrying)); + Assert.DoesNotContain(Fixture.TestSqlLoggerFactory.Log, l => l.Id == CoreEventId.ExecutionStrategyRetrying); } Assert.Equal(realFailure ? 3 : 2, connection.OpenCount); @@ -194,7 +194,7 @@ private async Task Test_commit_failure_async( { var connection = (TestNpgsqlConnection)context.GetService(); - connection.CommitFailures.Enqueue(new bool?[] { realFailure }); + connection.CommitFailures.Enqueue([realFailure]); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); @@ -213,7 +213,7 @@ private async Task Test_commit_failure_async( } else { - Assert.Empty(Fixture.TestSqlLoggerFactory.Log.Where(l => l.Id == CoreEventId.ExecutionStrategyRetrying)); + Assert.DoesNotContain(Fixture.TestSqlLoggerFactory.Log, l => l.Id == CoreEventId.ExecutionStrategyRetrying); } Assert.Equal(realFailure ? 3 : 2, connection.OpenCount); @@ -236,7 +236,7 @@ public void Handles_commit_failure_multiple_SaveChanges(bool realFailure) using (var context2 = CreateContext()) { - connection.CommitFailures.Enqueue(new bool?[] { realFailure }); + connection.CommitFailures.Enqueue([realFailure]); context1.Products.Add(new Product()); context2.Products.Add(new Product()); @@ -276,7 +276,7 @@ public async Task Retries_SaveChanges_on_execution_failure( { var connection = (TestNpgsqlConnection)context.GetService(); - connection.ExecutionFailures.Enqueue(new bool?[] { null, realFailure }); + connection.ExecutionFailures.Enqueue([null, realFailure]); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -385,7 +385,7 @@ public async Task Retries_query_on_execution_failure(bool externalStrategy, bool { var connection = (TestNpgsqlConnection)context.GetService(); - connection.ExecutionFailures.Enqueue(new bool?[] { true }); + connection.ExecutionFailures.Enqueue([true]); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -441,7 +441,7 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, { var connection = (TestNpgsqlConnection)context.GetService(); - connection.ExecutionFailures.Enqueue(new bool?[] { true }); + connection.ExecutionFailures.Enqueue([true]); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -453,14 +453,16 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, list = await new TestNpgsqlRetryingExecutionStrategy(context) .ExecuteAsync( context, (c, ct) => c.Set().FromSqlRaw( - @"SELECT ""Id"", ""Name"" - FROM ""Products""").ToListAsync(ct), null); + """ + SELECT "Id", "Name" FROM "Products" + """).ToListAsync(ct), null); } else { list = await context.Set().FromSqlRaw( - @"SELECT ""Id"", ""Name"" - FROM ""Products""").ToListAsync(); + """ + SELECT "Id", "Name" FROM "Products" + """).ToListAsync(); } } else @@ -470,14 +472,17 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, list = new TestNpgsqlRetryingExecutionStrategy(context) .Execute( context, c => c.Set().FromSqlRaw( - @"SELECT ""Id"", ""Name"" - FROM ""Products""").ToList(), null); + """ + SELECT "Id", "Name" + FROM "Products" + """).ToList(), null); } else { list = context.Set().FromSqlRaw( - @"SELECT ""Id"", ""Name"" - FROM ""Products""").ToList(); + """ + SELECT "Id", "Name" FROM "Products" + """).ToList(); } } @@ -496,7 +501,7 @@ public async Task Retries_OpenConnection_on_execution_failure(bool externalStrat await using var context = CreateContext(); var connection = (TestNpgsqlConnection)context.GetService(); - connection.OpenFailures.Enqueue(new bool?[] { true }); + connection.OpenFailures.Enqueue([true]); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -549,7 +554,7 @@ public async Task Retries_BeginTransaction_on_execution_failure(bool async) await using var context = CreateContext(); var connection = (TestNpgsqlConnection)context.GetService(); - connection.OpenFailures.Enqueue(new bool?[] { true }); + connection.OpenFailures.Enqueue([true]); Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -584,8 +589,8 @@ public void Verification_is_retried_using_same_retry_limit() { var connection = (TestNpgsqlConnection)context.GetService(); - connection.ExecutionFailures.Enqueue(new bool?[] { true, null, true, true }); - connection.CommitFailures.Enqueue(new bool?[] { true, true, true, true }); + connection.ExecutionFailures.Enqueue([true, null, true, true]); + connection.CommitFailures.Enqueue([true, true, true, true]); context.Products.Add(new Product()); Assert.Throws( @@ -607,13 +612,8 @@ public void Verification_is_retried_using_same_retry_limit() } } - protected class ExecutionStrategyContext : DbContext + protected class ExecutionStrategyContext(DbContextOptions options) : DbContext(options) { - public ExecutionStrategyContext(DbContextOptions options) - : base(options) - { - } - public DbSet Products { get; set; } } diff --git a/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs b/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs index cebec6272..abbe8f3f5 100644 --- a/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs +++ b/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs @@ -20,7 +20,7 @@ private static async Task Can_use_an_existing_closed_connection_test(bool openCo .AddEntityFrameworkNpgsql() .BuildServiceProvider(); - await using (var store = NpgsqlTestStore.GetNorthwindStore()) + await using (var store = await NpgsqlTestStore.GetNorthwindStoreAsync()) { store.CloseConnection(); @@ -67,16 +67,10 @@ private static async Task Can_use_an_existing_closed_connection_test(bool openCo } } - private class NorthwindContext : DbContext + private class NorthwindContext(IServiceProvider serviceProvider, NpgsqlConnection connection) : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly NpgsqlConnection _connection; - - public NorthwindContext(IServiceProvider serviceProvider, NpgsqlConnection connection) - { - _serviceProvider = serviceProvider; - _connection = connection; - } + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly NpgsqlConnection _connection = connection; // ReSharper disable once UnusedAutoPropertyAccessor.Local public DbSet Customers { get; set; } @@ -91,7 +85,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b => { b.HasKey(c => c.CustomerId); - RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)b, "Customers"); + ((EntityTypeBuilder)b).ToTable("Customers"); }); } diff --git a/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs index 193474eed..91be0a0e5 100644 --- a/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs @@ -21,16 +21,10 @@ protected override void BuildModelExternal(ModelBuilder modelBuilder) }); } - private class BytesToUIntConverter : ValueConverter - { - public BytesToUIntConverter() - : base( - bytes => BitConverter.ToUInt32(bytes), - num => BitConverter.GetBytes(num), - mappingHints: null) - { - } - } + private class BytesToUIntConverter() : ValueConverter( + bytes => BitConverter.ToUInt32(bytes), + num => BitConverter.GetBytes(num), + mappingHints: null); } public class F1NpgsqlFixture : F1NpgsqlFixtureBase diff --git a/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs index dd19464ba..c725a2f93 100644 --- a/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class FieldMappingNpgsqlTest - : FieldMappingTestBase +public class FieldMappingNpgsqlTest(FieldMappingNpgsqlTest.FieldMappingNpgsqlFixture fixture) + : FieldMappingTestBase(fixture) { - public FieldMappingNpgsqlTest(FieldMappingNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs index 071c2e844..a76837511 100644 --- a/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs @@ -10,33 +10,18 @@ protected FindNpgsqlTest(FindNpgsqlFixture fixture) fixture.TestSqlLoggerFactory.Clear(); } - public class FindNpgsqlTestSet : FindNpgsqlTest + public class FindNpgsqlTestSet(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) { - public FindNpgsqlTestSet(FindNpgsqlFixture fixture) - : base(fixture) - { - } - protected override TestFinder Finder { get; } = new FindViaSetFinder(); } - public class FindNpgsqlTestContext : FindNpgsqlTest + public class FindNpgsqlTestContext(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) { - public FindNpgsqlTestContext(FindNpgsqlFixture fixture) - : base(fixture) - { - } - protected override TestFinder Finder { get; } = new FindViaContextFinder(); } - public class FindNpgsqlTestNonGeneric : FindNpgsqlTest + public class FindNpgsqlTestNonGeneric(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) { - public FindNpgsqlTestNonGeneric(FindNpgsqlFixture fixture) - : base(fixture) - { - } - protected override TestFinder Finder { get; } = new FindViaNonGenericContextFinder(); } diff --git a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs index 2e1279923..1f15efa71 100644 --- a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs @@ -1,202 +1,228 @@ #nullable enable using System.Collections; - using System.Globalization; using System.Numerics; +using NetTopologySuite; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Xunit.Sdk; namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class JsonTypesNpgsqlTest : JsonTypesRelationalTestBase { - public override void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) - { - // Relational databases don't support unsigned numeric types, so ulong is value-converted to long - if (value == EnumU64.Max) - { - json = """{"Prop":-1}"""; - } - - base.Can_read_write_ulong_enum_JSON_values(value, json); - } + #region Nested collections (unsupported) - public override void Can_read_write_nullable_ulong_enum_JSON_values(object? value, string json) - { - // Relational databases don't support unsigned numeric types, so ulong is value-converted to long - if (Equals(value, ulong.MaxValue)) - { - json = """{"Prop":-1}"""; - } - - base.Can_read_write_nullable_ulong_enum_JSON_values(value, json); - } + // The following tests are disabled because they use nested collections, which are not supported by EFCore.PG (arrays of arrays aren't + // supported). - public override void Can_read_write_collection_of_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value>( - nameof(EnumU64CollectionType.EnumU64), - new List - { - EnumU64.Min, - EnumU64.Max, - EnumU64.Default, - EnumU64.One, - (EnumU64)8 - }, - // Relational databases don't support unsigned numeric types, so ulong is value-converted to long - """{"Prop":[0,-1,0,1,8]}""", - mappedCollection: true); - - public override void Can_read_write_collection_of_nullable_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value>( - nameof(NullableEnumU64CollectionType.EnumU64), - new List - { - EnumU64.Min, - null, - EnumU64.Max, - EnumU64.Default, - EnumU64.One, - (EnumU64?)8 - }, - // Relational databases don't support unsigned numeric types, so ulong is value-converted to long - """{"Prop":[0,null,-1,0,1,8]}""", - mappedCollection: true); + public override Task Can_read_write_array_of_array_of_array_of_int_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_array_of_array_of_int_JSON_values()); + + public override Task Can_read_write_array_of_list_of_array_of_IPAddress_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_array_of_IPAddress_JSON_values()); + + public override Task Can_read_write_array_of_list_of_array_of_string_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_array_of_string_JSON_values()); + + public override Task Can_read_write_array_of_list_of_binary_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_binary_JSON_values(expected)); + + public override Task Can_read_write_array_of_list_of_GUID_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_GUID_JSON_values(expected)); + + public override Task Can_read_write_array_of_list_of_int_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_int_JSON_values()); + + public override Task Can_read_write_array_of_list_of_IPAddress_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_IPAddress_JSON_values()); + + public override Task Can_read_write_array_of_list_of_string_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_string_JSON_values()); + + public override Task Can_read_write_array_of_list_of_ulong_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_array_of_list_of_ulong_JSON_values()); + + public override Task Can_read_write_list_of_array_of_GUID_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_GUID_JSON_values(expected)); + + public override Task Can_read_write_list_of_array_of_int_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_int_JSON_values()); + + public override Task Can_read_write_list_of_array_of_IPAddress_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_IPAddress_JSON_values()); + + public override Task Can_read_write_list_of_array_of_list_of_array_of_binary_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_list_of_array_of_binary_JSON_values(expected)); + + public override Task Can_read_write_list_of_array_of_list_of_IPAddress_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_list_of_IPAddress_JSON_values()); + + public override Task Can_read_write_list_of_array_of_list_of_string_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_list_of_string_JSON_values()); + + public override Task Can_read_write_list_of_array_of_list_of_ulong_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_list_of_ulong_JSON_values()); + + public override Task Can_read_write_list_of_array_of_nullable_GUID_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_nullable_GUID_JSON_values(expected)); + + public override Task Can_read_write_list_of_array_of_nullable_int_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_nullable_int_JSON_values()); + + public override Task Can_read_write_list_of_array_of_nullable_ulong_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_nullable_ulong_JSON_values()); + + public override Task Can_read_write_list_of_array_of_string_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_string_JSON_values()); + + public override Task Can_read_write_list_of_array_of_ulong_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_ulong_JSON_values()); + + public override Task Can_read_write_list_of_list_of_list_of_int_JSON_values() + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_list_of_list_of_int_JSON_values()); + + #endregion Nested collections (unsupported) + + // IEnumerable property + public override Task Can_read_write_list_of_array_of_binary_JSON_values(string expected) + => Assert.ThrowsAsync(() => base.Can_read_write_list_of_array_of_binary_JSON_values(expected)); + + // public override Task Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) + // { + // // Relational databases don't support unsigned numeric types, so ulong is value-converted to long + // if (value == EnumU64.Max) + // { + // json = """{"Prop":-1}"""; + // } + // + // return base.Can_read_write_ulong_enum_JSON_values(value, json); + // } + // + // public override void Can_read_write_nullable_ulong_enum_JSON_values(object? value, string json) + // { + // // Relational databases don't support unsigned numeric types, so ulong is value-converted to long + // if (Equals(value, ulong.MaxValue)) + // { + // json = """{"Prop":-1}"""; + // } + // + // base.Can_read_write_nullable_ulong_enum_JSON_values(value, json); + // } + // + // public override void Can_read_write_collection_of_ulong_enum_JSON_values() + // => Can_read_and_write_JSON_value>( + // nameof(EnumU64CollectionType.EnumU64), + // [ + // EnumU64.Min, + // EnumU64.Max, + // EnumU64.Default, + // EnumU64.One, + // (EnumU64)8 + // ], + // // Relational databases don't support unsigned numeric types, so ulong is value-converted to long + // """{"Prop":[0,-1,0,1,8]}""", + // mappedCollection: true); + // + // public override void Can_read_write_collection_of_nullable_ulong_enum_JSON_values() + // => Can_read_and_write_JSON_value>( + // nameof(NullableEnumU64CollectionType.EnumU64), + // [ + // EnumU64.Min, + // null, + // EnumU64.Max, + // EnumU64.Default, + // EnumU64.One, + // (EnumU64?)8 + // ], + // // Relational databases don't support unsigned numeric types, so ulong is value-converted to long + // """{"Prop":[0,null,-1,0,1,8]}""", + // mappedCollection: true); #region TimeSpan - public override void Can_read_write_TimeSpan_JSON_values(string value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. - } - - [ConditionalTheory] - [InlineData("-10675199.02:48:05.477580", """{"Prop":"-10675199 02:48:05.47758"}""")] - [InlineData("10675199.02:48:05.477580", """{"Prop":"10675199 02:48:05.47758"}""")] - [InlineData("00:00:00", """{"Prop":"00:00:00"}""")] - [InlineData("12:23:23.801885", """{"Prop":"12:23:23.801885"}""")] - public virtual void Can_read_write_TimeSpan_JSON_values_npgsql(string value, string json) - => Can_read_and_write_JSON_value( - nameof(TimeSpanType.TimeSpan), - TimeSpan.Parse(value, CultureInfo.InvariantCulture), - json); - - public override void Can_read_write_nullable_TimeSpan_JSON_values(string? value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. - } - - [ConditionalTheory] - [InlineData("-10675199.02:48:05.47758", """{"Prop":"-10675199 02:48:05.47758"}""")] - [InlineData("10675199.02:48:05.47758", """{"Prop":"10675199 02:48:05.47758"}""")] - [InlineData("00:00:00", """{"Prop":"00:00:00"}""")] - [InlineData("12:23:23.801885", """{"Prop":"12:23:23.801885"}""")] - [InlineData(null, """{"Prop":null}""")] - public virtual void Can_read_write_nullable_TimeSpan_JSON_values_npgsql(string? value, string json) - => Can_read_and_write_JSON_value( - nameof(NullableTimeSpanType.TimeSpan), - value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); - - [ConditionalFact] - public override void Can_read_write_collection_of_TimeSpan_JSON_values() - { - Can_read_and_write_JSON_value>( - nameof(TimeSpanCollectionType.TimeSpan), - new List - { - // We cannot roundtrip MinValue and MaxValue since these contain sub-microsecond components, which PG does not support. - // TimeSpan.MinValue, - new(1, 2, 3, 4), - new(0, 2, 3, 4, 5, 678), - // TimeSpan.MaxValue - }, - """{"Prop":["1 02:03:04","02:03:04.005678"]}""", - mappedCollection: true); - } - - [ConditionalFact] - public override void Can_read_write_collection_of_nullable_TimeSpan_JSON_values() - => Can_read_and_write_JSON_value>( - nameof(NullableTimeSpanCollectionType.TimeSpan), - new List - { - // We cannot roundtrip MinValue and MaxValue since these contain sub-microsecond components, which PG does not support. - // TimeSpan.MinValue, - new(1, 2, 3, 4), - new(0, 2, 3, 4, 5, 678), - // TimeSpan.MaxValue - null - }, - """{"Prop":["1 02:03:04","02:03:04.005678",null]}""", - mappedCollection: true); + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_TimeSpan_JSON_values_npgsql + public override Task Can_read_write_TimeSpan_JSON_values(string value, string json) + => Task.CompletedTask; + + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_TimeSpan_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_nullable_TimeSpan_JSON_values_npgsql + public override Task Can_read_write_nullable_TimeSpan_JSON_values(string? value, string json) + => Task.CompletedTask; + + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_TimeSpan_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_collection_of_TimeSpan_JSON_values_npgsql + public override Task Can_read_write_collection_of_TimeSpan_JSON_values() + => Task.CompletedTask; + + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_TimeSpan_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_collection_of_nullable_TimeSpan_JSON_values_npgsql + public override Task Can_read_write_collection_of_nullable_TimeSpan_JSON_values() + => Task.CompletedTask; #endregion TimeSpan #region DateOnly - public override void Can_read_write_DateOnly_JSON_values(string value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateOnly_JSON_values_npgsql instead. + public override Task Can_read_write_DateOnly_JSON_values(string value, string json) + => Task.CompletedTask; [ConditionalTheory] [InlineData("1/1/0001", """{"Prop":"-infinity"}""")] [InlineData("12/31/9999", """{"Prop":"infinity"}""")] [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] - public virtual void Can_read_write_DateOnly_JSON_values_npgsql(string value, string json) + public virtual Task Can_read_write_DateOnly_JSON_values_npgsql(string value, string json) => Can_read_and_write_JSON_value( nameof(DateOnlyType.DateOnly), DateOnly.Parse(value, CultureInfo.InvariantCulture), json); - public override void Can_read_write_nullable_DateOnly_JSON_values(string? value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_DateOnly_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_DateOnly_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_nullable_DateOnly_JSON_values_npgsql + public override Task Can_read_write_nullable_DateOnly_JSON_values(string? value, string json) + => Task.CompletedTask; - [ConditionalTheory] - [InlineData("1/1/0001", """{"Prop":"-infinity"}""")] - [InlineData("12/31/9999", """{"Prop":"infinity"}""")] - [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] - [InlineData(null, """{"Prop":null}""")] - public virtual void Can_read_write_nullable_DateOnly_JSON_values_npgsql(string? value, string json) - => Can_read_and_write_JSON_value( - nameof(NullableDateOnlyType.DateOnly), - value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_npgsql instead. + public override Task Can_read_write_collection_of_DateOnly_JSON_values() + => Task.CompletedTask; [ConditionalFact] - public virtual void Can_read_write_DateOnly_JSON_values_infinity() - { - Can_read_and_write_JSON_value(nameof(DateOnlyType.DateOnly), DateOnly.MinValue, """{"Prop":"-infinity"}"""); - Can_read_and_write_JSON_value(nameof(DateOnlyType.DateOnly), DateOnly.MaxValue, """{"Prop":"infinity"}"""); - } - - [ConditionalFact] - public override void Can_read_write_collection_of_DateOnly_JSON_values() - => Can_read_and_write_JSON_value>( + public virtual Task Can_read_write_collection_of_DateOnly_JSON_values_npgsql() + => Can_read_and_write_JSON_value>( nameof(DateOnlyCollectionType.DateOnly), - new List - { + [ DateOnly.MinValue, new(2023, 5, 29), DateOnly.MaxValue - }, + ], """{"Prop":["-infinity","2023-05-29","infinity"]}""", mappedCollection: true); + protected class NpgsqlDateOnlyCollectionType + { + public List DateOnly { get; set; } = null!; + } + [ConditionalFact] - public override void Can_read_write_collection_of_nullable_DateOnly_JSON_values() + public override Task Can_read_write_collection_of_nullable_DateOnly_JSON_values() => Can_read_and_write_JSON_value>( nameof(NullableDateOnlyCollectionType.DateOnly), - new List - { + [ DateOnly.MinValue, new(2023, 5, 29), DateOnly.MaxValue, null - }, + ], """{"Prop":["-infinity","2023-05-29","infinity",null]}""", mappedCollection: true); @@ -204,164 +230,142 @@ public override void Can_read_write_collection_of_nullable_DateOnly_JSON_values( #region DateTime - public override void Can_read_write_DateTime_JSON_values(string value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateTime_JSON_values_npgsql instead. + public override Task Can_read_write_DateTime_JSON_values(string value, string json) + => Task.CompletedTask; [ConditionalTheory] - [InlineData("2023-05-29T10:52:47.206435Z", """{"Prop":"2023-05-29T10:52:47.206435Z"}""")] - public virtual void Can_read_write_DateTime_JSON_values_npgsql(string value, string json) + [InlineData("0001-01-01T00:00:00.0000000", """{"Prop":"-infinity"}""")] + [InlineData("9999-12-31T23:59:59.9999999", """{"Prop":"infinity"}""")] + [InlineData("2023-05-29T10:52:47.2064350", """{"Prop":"2023-05-29T10:52:47.206435"}""")] + public virtual Task Can_read_write_DateTime_JSON_values_npgsql(string value, string json) => Can_read_and_write_JSON_value( + modelBuilder => modelBuilder + .Entity() + .HasNoKey() + .Property(nameof(DateTimeType.DateTime)) + .HasColumnType("timestamp without time zone"), + configureConventions: null, nameof(DateTimeType.DateTime), - DateTime.Parse(value, CultureInfo.InvariantCulture).ToUniversalTime(), json); + DateTime.Parse(value, CultureInfo.InvariantCulture), json); - [ConditionalFact] - public virtual void Can_read_write_DateTime_JSON_values_npgsql_infinity() - => Can_read_and_write_JSON_value( - nameof(DateTimeType.DateTime), - DateTime.MaxValue, """{"Prop":"infinity"}"""); + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_DateTime_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_nullable_DateTime_JSON_values_npgsql + public override Task Can_read_write_nullable_DateTime_JSON_values(string? value, string json) + => Task.CompletedTask; - [ConditionalFact] - public virtual void Can_read_write_DateTime_JSON_values_npgsql_negative_infinity() - => Can_read_and_write_JSON_value( - nameof(DateTimeType.DateTime), - DateTime.MinValue, """{"Prop":"-infinity"}"""); - - public override void Can_read_write_nullable_DateTime_JSON_values(string? value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_npgsql instead. + public override Task Can_read_write_collection_of_DateTime_JSON_values(string expected) + => Task.CompletedTask; [ConditionalTheory] - [InlineData("0001-01-01T00:00:00.0000000", """{"Prop":"-infinity"}""")] - [InlineData("9999-12-31T23:59:59.9999999", """{"Prop":"infinity"}""")] - [InlineData("2023-05-29T10:52:47.206435", """{"Prop":"2023-05-29T10:52:47.206435Z"}""")] - [InlineData(null, """{"Prop":null}""")] - public virtual void Can_read_write_nullable_DateTime_JSON_values_npgsql(string? value, string json) - => Can_read_and_write_JSON_value( - nameof(NullableDateTimeType.DateTime), - value == null - ? default(DateTime?) - : DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), json); - - [ConditionalFact] - public override void Can_read_write_collection_of_DateTime_JSON_values() - => Can_read_and_write_JSON_value>( + [InlineData("""{"Prop":["-infinity","2023-05-29T10:52:47","infinity"]}""")] + public virtual Task Can_read_write_collection_of_DateTime_JSON_values_npgsql(string expected) + => Can_read_and_write_JSON_value>( + modelBuilder => modelBuilder + .Entity() + .HasNoKey() + .PrimitiveCollection(nameof(DateTimeCollectionType.DateTime)) + .ElementType() + .HasStoreType("timestamp without time zone"), + configureConventions: null, nameof(DateTimeCollectionType.DateTime), - new List - { + [ DateTime.MinValue, - new(2023, 5, 29, 10, 52, 47, DateTimeKind.Utc), + new(2023, 5, 29, 10, 52, 47), DateTime.MaxValue - }, - """{"Prop":["-infinity","2023-05-29T10:52:47Z","infinity"]}""", + ], + expected, mappedCollection: true); - [ConditionalFact] - public override void Can_read_write_collection_of_nullable_DateTime_JSON_values() + protected class NpgsqlDateTimeCollectionType + { + public List DateTime { get; set; } = null!; + } + + public override Task Can_read_write_collection_of_nullable_DateTime_JSON_values(string expected) => Can_read_and_write_JSON_value>( + modelBuilder => modelBuilder + .Entity() + .HasNoKey() + .PrimitiveCollection(nameof(NullableDateTimeCollectionType.DateTime)) + .ElementType() + .HasStoreType("timestamp without time zone"), + configureConventions: null, nameof(NullableDateTimeCollectionType.DateTime), - new List - { + [ DateTime.MinValue, null, - new(2023, 5, 29, 10, 52, 47, DateTimeKind.Utc), + new(2023, 5, 29, 10, 52, 47), DateTime.MaxValue - }, - """{"Prop":["-infinity",null,"2023-05-29T10:52:47Z","infinity"]}""", + ], + """{"Prop":["-infinity",null,"2023-05-29T10:52:47","infinity"]}""", mappedCollection: true); - [ConditionalFact] - public virtual void Can_read_write_DateTime_timestamptz_JSON_values_infinity() - { - Can_read_and_write_JSON_value(nameof(DateTimeType.DateTime), DateTime.MinValue, """{"Prop":"-infinity"}"""); - Can_read_and_write_JSON_value(nameof(DateTimeType.DateTime), DateTime.MaxValue, """{"Prop":"infinity"}"""); - } - - [ConditionalFact] - public virtual void Can_read_write_DateTime_timestamp_JSON_values_infinity() - { - Can_read_and_write_JSON_property_value( - b => b.HasColumnType("timestamp without time zone"), - nameof(DateTimeType.DateTime), - DateTime.MinValue, - """{"Prop":"-infinity"}"""); - - Can_read_and_write_JSON_property_value( - b => b.HasColumnType("timestamp without time zone"), - nameof(DateTimeType.DateTime), - DateTime.MaxValue, - """{"Prop":"infinity"}"""); - } - #endregion DateTime #region DateTimeOffset - public override void Can_read_write_DateTimeOffset_JSON_values(string value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_DateTimeOffset_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateTimeOffset_JSON_values_npgsql instead. + public override Task Can_read_write_DateTimeOffset_JSON_values(string value, string json) + => Task.CompletedTask; [ConditionalTheory] - [InlineData("0001-01-01T00:00:00.000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] - [InlineData("9999-12-31T23:59:59.999999+02:00", """{"Prop":"9999-12-31T23:59:59.999999\u002B02:00"}""")] - [InlineData("0001-01-01T00:00:00.000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] - [InlineData("2023-05-29T11:11:15.567285+04:00", """{"Prop":"2023-05-29T11:11:15.567285\u002B04:00"}""")] - public virtual void Can_read_write_DateTimeOffset_JSON_values_npgsql(string value, string json) + [InlineData("0001-01-01T00:00:00.0000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] + [InlineData("9999-12-31T23:59:59.9999990+02:00", """{"Prop":"9999-12-31T23:59:59.999999\u002B02:00"}""")] + [InlineData("0001-01-01T00:00:00.0000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] + [InlineData("2023-05-29T11:11:15.5672850+04:00", """{"Prop":"2023-05-29T11:11:15.567285\u002B04:00"}""")] + public virtual Task Can_read_write_DateTimeOffset_JSON_values_npgsql(string value, string json) => Can_read_and_write_JSON_value( nameof(DateTimeOffsetType.DateTimeOffset), DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); - public override void Can_read_write_nullable_DateTimeOffset_JSON_values(string? value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_DateTimeOffset_JSON_values_sqlite instead. - } + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_nullable_DateTimeOffset_JSON_values_npgsql instead. + // TODO: Implement Can_read_write_nullable_DateTimeOffset_JSON_values_npgsql + public override Task Can_read_write_nullable_DateTimeOffset_JSON_values(string? value, string json) + => Task.CompletedTask; - [ConditionalTheory] - [InlineData("0001-01-01T00:00:00.000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] - [InlineData("9999-12-31T23:59:59.999999+02:00", """{"Prop":"9999-12-31T23:59:59.999999\u002B02:00"}""")] - [InlineData("0001-01-01T00:00:00.000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] - [InlineData("2023-05-29T11:11:15.567285+04:00", """{"Prop":"2023-05-29T11:11:15.567285\u002B04:00"}""")] - [InlineData(null, """{"Prop":null}""")] - public virtual void Can_read_write_nullable_DateTimeOffset_JSON_values_npgsql(string? value, string json) - => Can_read_and_write_JSON_value( - nameof(NullableDateTimeOffsetType.DateTimeOffset), - value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_collection_of_DateTimeOffset_JSON_values_npgsql instead. + public override Task Can_read_write_collection_of_DateTimeOffset_JSON_values(string expected) + => Task.CompletedTask; [ConditionalFact] - public override void Can_read_write_collection_of_DateTimeOffset_JSON_values() + public virtual Task Can_read_write_collection_of_DateTimeOffset_JSON_values_npgsql() => Can_read_and_write_JSON_value>( nameof(DateTimeOffsetCollectionType.DateTimeOffset), - new List - { + [ DateTimeOffset.MinValue, new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(-2, 0, 0)), new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(0, 0, 0)), new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), DateTimeOffset.MaxValue - }, + ], """{"Prop":["-infinity","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47\u002B00:00","2023-05-29T10:52:47\u002B02:00","infinity"]}""", mappedCollection: true); + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values_npgsql instead. + public override Task Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values(string expected) + => Task.CompletedTask; + [ConditionalFact] - public override void Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values() + public virtual Task Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values_npgsql() => Can_read_and_write_JSON_value>( nameof(NullableDateTimeOffsetCollectionType.DateTimeOffset), - new List - { + [ DateTimeOffset.MinValue, new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(-2, 0, 0)), new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(0, 0, 0)), null, new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), DateTimeOffset.MaxValue - }, + ], """{"Prop":["-infinity","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47\u002B00:00",null,"2023-05-29T10:52:47\u002B02:00","infinity"]}""", mappedCollection: true); @@ -379,7 +383,7 @@ public virtual void Can_read_write_timetz_JSON_values(string value, string json) [ConditionalTheory] [InlineData(Mood.Happy, """{"Prop":"Happy"}""")] [InlineData(Mood.Sad, """{"Prop":"Sad"}""")] - public virtual void Can_read_write_pg_enum_JSON_values(Mood value, string json) + public virtual Task Can_read_write_pg_enum_JSON_values(Mood value, string json) => Can_read_and_write_JSON_value( nameof(EnumType.Mood), value, @@ -399,7 +403,7 @@ public enum Mood [ConditionalTheory] [InlineData(new[] { 1, 2, 3 }, """{"Prop":[1,2,3]}""")] [InlineData(new int[0], """{"Prop":[]}""")] - public virtual void Can_read_write_array_JSON_values(int[] value, string json) + public virtual Task Can_read_write_array_JSON_values(int[] value, string json) => Can_read_and_write_JSON_value( nameof(ArrayType.Array), value, @@ -412,9 +416,9 @@ protected class ArrayType } [ConditionalFact] - public virtual void Cannot_read_write_multidimensional_array_JSON_values() + public virtual Task Cannot_read_write_multidimensional_array_JSON_values() // EF currently throws NRE when the type mapping has no JsonValueReaderWriter (this has been improved for 9.0) - => Assert.Throws( + => Assert.ThrowsAsync( () => Can_read_and_write_JSON_value( nameof(MultidimensionalArrayType.MultidimensionalArray), new[,] { { 1, 2 }, { 3, 4 } }, @@ -426,7 +430,7 @@ protected class MultidimensionalArrayType } [ConditionalFact] - public virtual void Can_read_write_BigInteger_JSON_values() + public virtual Task Can_read_write_BigInteger_JSON_values() => Can_read_and_write_JSON_value( nameof(BigIntegerType.BigInteger), new BigInteger(ulong.MaxValue), @@ -441,7 +445,7 @@ protected class BigIntegerType [InlineData(new[] { true, false, true }, """{"Prop":"101"}""")] [InlineData(new[] { true, false, true, true, false, true, false, true, false }, """{"Prop":"101101010"}""")] [InlineData(new bool[0], """{"Prop":""}""")] - public virtual void Can_read_write_BitArray_JSON_values(bool[] value, string json) + public virtual Task Can_read_write_BitArray_JSON_values(bool[] value, string json) => Can_read_and_write_JSON_value( nameof(BitArrayType.BitArray), new BitArray(value), @@ -455,26 +459,125 @@ protected class BitArrayType [ConditionalTheory] [InlineData(1000, """{"Prop":"0/3E8"}""")] [InlineData(0, """{"Prop":"0/0"}""")] - public virtual void Can_read_write_LogSequenceNumber_JSON_values(ulong value, string json) + public virtual Task Can_read_write_LogSequenceNumber_JSON_values(ulong value, string json) => Can_read_and_write_JSON_value( nameof(LogSequenceNumberType.LogSequenceNumber), new NpgsqlLogSequenceNumber(value), json); + [ConditionalFact] + public override async Task Can_read_write_point() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(PointType.Point), + factory.CreatePoint(new Coordinate(2, 4)), + """{"Prop":"SRID=4326;POINT (2 4)"}"""); + } + + [ConditionalFact] + public virtual async Task Can_read_write_point_without_SRID() + => await Can_read_and_write_JSON_value( + nameof(PointType.Point), + new Point(2, 4), + """{"Prop":"POINT (2 4)"}"""); + + [ConditionalFact] + public override async Task Can_read_write_point_with_M() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(PointMType.PointM), + factory.CreatePoint(new CoordinateM(2, 4, 6)), + """{"Prop":"SRID=4326;POINT (2 4)"}"""); + } + + public override async Task Can_read_write_point_with_Z() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(PointZType.PointZ), + factory.CreatePoint(new CoordinateZ(2, 4, 6)), + """{"Prop":"SRID=4326;POINT Z(2 4 6)"}"""); + } + + public override async Task Can_read_write_point_with_Z_and_M() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(PointZMType.PointZM), + factory.CreatePoint(new CoordinateZM(1, 2, 3, 4)), + """{"Prop":"SRID=4326;POINT Z(1 2 3)"}"""); + } + + [ConditionalFact] + public override async Task Can_read_write_line_string() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(LineStringType.LineString), + factory.CreateLineString([new Coordinate(0, 0), new Coordinate(1, 0)]), + """{"Prop":"SRID=4326;LINESTRING (0 0, 1 0)"}"""); + } + + public override async Task Can_read_write_multi_line_string() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(MultiLineStringType.MultiLineString), + factory.CreateMultiLineString( + [ + factory.CreateLineString( + [new Coordinate(0, 0), new Coordinate(0, 1)]), + factory.CreateLineString( + [new Coordinate(1, 0), new Coordinate(1, 1)]) + ]), + """{"Prop":"SRID=4326;MULTILINESTRING ((0 0, 0 1), (1 0, 1 1))"}"""); + } + + public override async Task Can_read_write_polygon() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(PolygonType.Polygon), + factory.CreatePolygon([new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0)]), + """{"Prop":"SRID=4326;POLYGON ((0 0, 1 0, 0 1, 0 0))"}"""); + } + + public override async Task Can_read_write_polygon_typed_as_geometry() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + await Can_read_and_write_JSON_value( + nameof(GeometryType.Geometry), + factory.CreatePolygon([new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0)]), + """{"Prop":"SRID=4326;POLYGON ((0 0, 1 0, 0 1, 0 0))"}"""); + } + protected class LogSequenceNumberType { public NpgsqlLogSequenceNumber LogSequenceNumber { get; set; } } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => base.OnConfiguring(optionsBuilder.UseNpgsql(b => b.UseNetTopologySuite())); + protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkNpgsqlNetTopologySuite(); - static JsonTypesNpgsqlTest() + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { -#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/KeysWithConvertersNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/KeysWithConvertersNpgsqlTest.cs index 1bc6bf9af..e83af54b8 100644 --- a/test/EFCore.PG.FunctionalTests/KeysWithConvertersNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/KeysWithConvertersNpgsqlTest.cs @@ -2,14 +2,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class KeysWithConvertersNpgsqlTest : KeysWithConvertersTestBase< - KeysWithConvertersNpgsqlTest.KeysWithConvertersNpgsqlFixture> +public class KeysWithConvertersNpgsqlTest(KeysWithConvertersNpgsqlTest.KeysWithConvertersNpgsqlFixture fixture) + : KeysWithConvertersTestBase< + KeysWithConvertersNpgsqlTest.KeysWithConvertersNpgsqlFixture>(fixture) { - public KeysWithConvertersNpgsqlTest(KeysWithConvertersNpgsqlFixture fixture) - : base(fixture) - { - } - public class KeysWithConvertersNpgsqlFixture : KeysWithConvertersFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs index 5432f3364..a495a96cb 100644 --- a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs @@ -18,6 +18,7 @@ public void Logs_context_initialization_postgres_version() ExpectedMessage($"PostgresVersion=10.7 {DefaultOptions}"), ActualMessage(s => CreateOptionsBuilder(s, b => ((NpgsqlDbContextOptionsBuilder)b).SetPostgresVersion(Version.Parse("10.7"))))); +#pragma warning disable CS0618 // Authentication APIs on NpgsqlDbContextOptionsBuilder are obsolete [Fact] public void Logs_context_initialization_provide_client_certificates_callback() => Assert.Equal( @@ -42,6 +43,7 @@ public void Logs_context_initialization_remote_certificate_validation_callback() s => CreateOptionsBuilder( s, b => ((NpgsqlDbContextOptionsBuilder)b).RemoteCertificateValidationCallback((_, _, _, _) => true)))); +#pragma warning restore CS0618 // Authentication APIs on NpgsqlDbContextOptionsBuilder are obsolete [Fact] public void Logs_context_initialization_reverse_null_ordering() diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs index 9f4b69706..e56daca7a 100644 --- a/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs @@ -3,15 +3,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class ManyToManyFieldsLoadNpgsqlTest - : ManyToManyFieldsLoadTestBase +public class ManyToManyFieldsLoadNpgsqlTest(ManyToManyFieldsLoadNpgsqlTest.ManyToManyFieldsLoadNpgsqlFixture fixture) + : ManyToManyFieldsLoadTestBase(fixture) { - public ManyToManyFieldsLoadNpgsqlTest(ManyToManyFieldsLoadNpgsqlFixture fixture) - : base(fixture) - { - } - - public class ManyToManyFieldsLoadNpgsqlFixture : ManyToManyFieldsLoadFixtureBase + public class ManyToManyFieldsLoadNpgsqlFixture : ManyToManyFieldsLoadFixtureBase, ITestSqlLoggerFactory { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs index 0d7eaddd4..c25a46d38 100644 --- a/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs @@ -3,14 +3,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class ManyToManyLoadNpgsqlTest : ManyToManyLoadTestBase +public class ManyToManyLoadNpgsqlTest(ManyToManyLoadNpgsqlTest.ManyToManyLoadNpgsqlFixture fixture) + : ManyToManyLoadTestBase(fixture) { - public ManyToManyLoadNpgsqlTest(ManyToManyLoadNpgsqlFixture fixture) - : base(fixture) - { - } - - public class ManyToManyLoadNpgsqlFixture : ManyToManyLoadFixtureBase + public class ManyToManyLoadNpgsqlFixture : ManyToManyLoadFixtureBase, ITestSqlLoggerFactory { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs index 3ef930e16..9faf5f107 100644 --- a/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs @@ -3,22 +3,21 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class ManyToManyTrackingNpgsqlTest : ManyToManyTrackingRelationalTestBase< - ManyToManyTrackingNpgsqlTest.ManyToManyTrackingNpgsqlFixture> +public class ManyToManyTrackingNpgsqlTest(ManyToManyTrackingNpgsqlTest.ManyToManyTrackingNpgsqlFixture fixture) + : ManyToManyTrackingRelationalTestBase< + ManyToManyTrackingNpgsqlTest.ManyToManyTrackingNpgsqlFixture>(fixture) { - public ManyToManyTrackingNpgsqlTest(ManyToManyTrackingNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); - public class ManyToManyTrackingNpgsqlFixture : ManyToManyTrackingRelationalFixture + public class ManyToManyTrackingNpgsqlFixture : ManyToManyTrackingRelationalFixture, ITestSqlLoggerFactory { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs index dc8e30903..1e44c27db 100644 --- a/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs @@ -1,27 +1,14 @@ #nullable enable -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class MaterializationInterceptionNpgsqlTest : - MaterializationInterceptionTestBase, - IClassFixture + MaterializationInterceptionTestBase { - public MaterializationInterceptionNpgsqlTest(MaterializationInterceptionNpgsqlFixture fixture) - : base(fixture) + public class NpgsqlLibraryContext(DbContextOptions options) : LibraryContext(options) { - } - - public class SqlServerLibraryContext : LibraryContext - { - public SqlServerLibraryContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -33,27 +20,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } - public override LibraryContext CreateContext(IEnumerable interceptors, bool inject) - => new SqlServerLibraryContext(Fixture.CreateOptions(interceptors, inject)); - - public class MaterializationInterceptionNpgsqlFixture : SingletonInterceptorsFixtureBase - { - protected override string StoreName - => "MaterializationInterception"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override IServiceCollection InjectInterceptors( - IServiceCollection serviceCollection, - IEnumerable injectedInterceptors) - => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)) - .ExecutionStrategy(d => new NpgsqlExecutionStrategy(d)); - return builder; - } - } + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs index 6336588d7..542d3ddda 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs @@ -4,14 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations { - public class MigrationsInfrastructureNpgsqlTest - : MigrationsInfrastructureTestBase + public class MigrationsInfrastructureNpgsqlTest(MigrationsInfrastructureNpgsqlTest.MigrationsInfrastructureNpgsqlFixture fixture) + : MigrationsInfrastructureTestBase(fixture) { - public MigrationsInfrastructureNpgsqlTest(MigrationsInfrastructureNpgsqlFixture fixture) - : base(fixture) - { - } - public override void Can_get_active_provider() { base.Can_get_active_provider(); @@ -19,12 +14,48 @@ public override void Can_get_active_provider() Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", ActiveProvider); } + // See #3407 + public override void Can_apply_two_migrations_in_transaction() + => Assert.ThrowsAny(() => base.Can_apply_two_migrations_in_transaction()); + + // See #3407 + public override Task Can_apply_two_migrations_in_transaction_async() + => Assert.ThrowsAnyAsync(() => base.Can_apply_two_migrations_in_transaction_async()); + + // This tests uses Fixture.CreateEmptyContext(), which does not go through MigrationsInfrastructureNpgsqlFixture.CreateContext() + // and therefore does not set the PostgresVersion in the context options. As a result, we try to drop the database with + // WITH (FORCE), which is only supported starting with PG 13. + [MinimumPostgresVersion(13, 0)] + public override Task Can_generate_no_migration_script() + => base.Can_generate_no_migration_script(); + + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] + public override void Can_apply_all_migrations() + => base.Can_apply_all_migrations(); + + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] + public override void Can_apply_range_of_migrations() + => base.Can_apply_range_of_migrations(); + + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] + public override void Can_revert_all_migrations() + => base.Can_revert_all_migrations(); + + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] + public override void Can_revert_one_migrations() + => base.Can_revert_one_migrations(); + + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/33056")] + public override Task Can_apply_all_migrations_async() + => base.Can_apply_all_migrations_async(); + [ConditionalFact] public async Task Empty_Migration_Creates_Database() { await using var context = new BloggingContext( Fixture.TestStore.AddProviderOptions( - new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)) + .ConfigureWarnings(e => e.Log(RelationalEventId.PendingModelChangesWarning)).Options); var creator = (NpgsqlDatabaseCreator)context.GetService(); creator.RetryTimeout = TimeSpan.FromMinutes(10); @@ -34,13 +65,8 @@ public async Task Empty_Migration_Creates_Database() Assert.True(creator.Exists()); } - private class BloggingContext : DbContext + private class BloggingContext(DbContextOptions options) : DbContext(options) { - public BloggingContext(DbContextOptions options) - : base(options) - { - } - // ReSharper disable once UnusedMember.Local public DbSet Blogs { get; set; } @@ -148,6 +174,9 @@ public override void Can_diff_against_2_1_ASP_NET_Identity_model() // TODO: Implement } + protected override Task ExecuteSqlAsync(string value) + => ((NpgsqlTestStore)Fixture.TestStore).ExecuteNonQueryAsync(value); + public class MigrationsInfrastructureNpgsqlFixture : MigrationsInfrastructureFixtureBase { protected override ITestStoreFactory TestStoreFactory @@ -156,21 +185,14 @@ protected override ITestStoreFactory TestStoreFactory public override MigrationsContext CreateContext() { var options = AddOptions( - new DbContextOptionsBuilder() + TestStore.AddProviderOptions(new DbContextOptionsBuilder()) .UseNpgsql( TestStore.ConnectionString, b => b.ApplyConfiguration() - .CommandTimeout(NpgsqlTestStore.CommandTimeout) - .SetPostgresVersion(TestEnvironment.PostgresVersion) - .ReverseNullOrdering())) - .UseInternalServiceProvider(CreateServiceProvider()) + .SetPostgresVersion(TestEnvironment.PostgresVersion))) + .UseInternalServiceProvider(ServiceProvider) .Options; return new MigrationsContext(options); } - - private static IServiceProvider CreateServiceProvider() - => new ServiceCollection() - .AddEntityFrameworkNpgsql() - .BuildServiceProvider(); } } } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 0ffca7ec7..aff93c789 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -62,7 +62,7 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'dbo2') THEN COMMENT ON COLUMN dbo2."People"."EmployerId" IS 'Employer ID comment'; """, // - @"CREATE INDEX ""IX_People_EmployerId"" ON dbo2.""People"" (""EmployerId"");"); + """CREATE INDEX "IX_People_EmployerId" ON dbo2."People" ("EmployerId");"""); } public override async Task Create_table_no_key() @@ -391,40 +391,35 @@ public override async Task Drop_table() { await base.Drop_table(); - AssertSql( - @"DROP TABLE ""People"";"); + AssertSql("""DROP TABLE "People";"""); } public override async Task Alter_table_add_comment() { await base.Alter_table_add_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS 'Table comment';"); + AssertSql("""COMMENT ON TABLE "People" IS 'Table comment';"""); } public override async Task Alter_table_add_comment_non_default_schema() { await base.Alter_table_add_comment_non_default_schema(); - AssertSql( - @"COMMENT ON TABLE ""SomeOtherSchema"".""People"" IS 'Table comment';"); + AssertSql("""COMMENT ON TABLE "SomeOtherSchema"."People" IS 'Table comment';"""); } public override async Task Alter_table_change_comment() { await base.Alter_table_change_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS 'Table comment2';"); + AssertSql("""COMMENT ON TABLE "People" IS 'Table comment2';"""); } public override async Task Alter_table_remove_comment() { await base.Alter_table_remove_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS NULL;"); + AssertSql("""COMMENT ON TABLE "People" IS NULL;"""); } [Fact] @@ -478,8 +473,7 @@ await Test( builder => builder.Entity("People").IsUnlogged(), asserter: null); // We don't scaffold unlogged - AssertSql( - @"ALTER TABLE ""People"" SET UNLOGGED;"); + AssertSql("""ALTER TABLE "People" SET UNLOGGED;"""); } [Fact] @@ -496,8 +490,7 @@ await Test( _ => { }, asserter: null); // We don't scaffold unlogged - AssertSql( - @"ALTER TABLE ""People"" SET LOGGED;"); + AssertSql("""ALTER TABLE "People" SET LOGGED;"""); } public override async Task Rename_table() @@ -505,11 +498,11 @@ public override async Task Rename_table() await base.Rename_table(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" RENAME TO ""Persons"";", + """ALTER TABLE "People" RENAME TO "Persons";""", // - @"ALTER TABLE ""Persons"" ADD CONSTRAINT ""PK_Persons"" PRIMARY KEY (""Id"");"); + """ALTER TABLE "Persons" ADD CONSTRAINT "PK_Persons" PRIMARY KEY ("Id");"""); } public override async Task Rename_table_with_primary_key() @@ -517,11 +510,11 @@ public override async Task Rename_table_with_primary_key() await base.Rename_table_with_primary_key(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" RENAME TO ""Persons"";", + """ALTER TABLE "People" RENAME TO "Persons";""", // - @"ALTER TABLE ""Persons"" ADD CONSTRAINT ""PK_Persons"" PRIMARY KEY (""Id"");"); + """ALTER TABLE "Persons" ADD CONSTRAINT "PK_Persons" PRIMARY KEY ("Id");"""); } public override async Task Move_table() @@ -538,7 +531,7 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'TestTableSchema') THEN END $EF$; """, // - @"ALTER TABLE ""TestTable"" SET SCHEMA ""TestTableSchema"";"); + """ALTER TABLE "TestTable" SET SCHEMA "TestTableSchema";"""); } #endregion @@ -594,8 +587,7 @@ public override async Task Add_column_with_defaultValue_string() { await base.Add_column_with_defaultValue_string(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text NOT NULL DEFAULT 'John Doe';"); + AssertSql("""ALTER TABLE "People" ADD "Name" text NOT NULL DEFAULT 'John Doe';"""); } public override async Task Add_column_with_defaultValue_datetime() @@ -614,8 +606,7 @@ await Test( Assert.False(birthdayColumn.IsNullable); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Birthday"" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12T17:05:00Z';"); + AssertSql("""ALTER TABLE "People" ADD "Birthday" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12T17:05:00Z';"""); } [Fact] @@ -623,8 +614,7 @@ public override async Task Add_column_with_defaultValueSql() { await base.Add_column_with_defaultValueSql(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Sum"" integer NOT NULL DEFAULT (1 + 2);"); + AssertSql("""ALTER TABLE "People" ADD "Sum" integer NOT NULL DEFAULT (1 + 2);"""); } public override async Task Add_column_with_computedSql(bool? stored) @@ -644,40 +634,35 @@ public override async Task Add_column_with_computedSql(bool? stored) await base.Add_column_with_computedSql(stored: true); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Sum"" text GENERATED ALWAYS AS (""X"" + ""Y"") STORED;"); + AssertSql("""ALTER TABLE "People" ADD "Sum" text GENERATED ALWAYS AS ("X" + "Y") STORED;"""); } public override async Task Add_column_with_required() { await base.Add_column_with_required(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text NOT NULL DEFAULT '';"); + AssertSql("""ALTER TABLE "People" ADD "Name" text NOT NULL DEFAULT '';"""); } public override async Task Add_column_with_ansi() { await base.Add_column_with_ansi(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } public override async Task Add_column_with_max_length() { await base.Add_column_with_max_length(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" character varying(30);"); + AssertSql("""ALTER TABLE "People" ADD "Name" character varying(30);"""); } public override async Task Add_column_with_unbounded_max_length() { await base.Add_column_with_unbounded_max_length(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } public override async Task Add_column_with_max_length_on_derived() @@ -691,7 +676,7 @@ public override async Task Add_column_with_fixed_length() { await base.Add_column_with_fixed_length(); - AssertSql(@"ALTER TABLE ""People"" ADD ""Name"" character(100);"); + AssertSql("""ALTER TABLE "People" ADD "Name" character(100);"""); } public override async Task Add_column_with_comment() @@ -709,8 +694,7 @@ public override async Task Add_column_with_collation() { await base.Add_column_with_collation(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "POSIX";"""); } public override async Task Add_column_computed_with_collation(bool stored) @@ -730,59 +714,9 @@ public override async Task Add_column_computed_with_collation(bool stored) await base.Add_column_computed_with_collation(stored); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"" GENERATED ALWAYS AS ('hello') STORED;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "POSIX" GENERATED ALWAYS AS ('hello') STORED;"""); } -#pragma warning disable CS0618 - [ConditionalFact] - public async Task Add_column_with_default_column_collation() - { - await Test( - builder => - { - builder.UseDefaultColumnCollation("POSIX"); - builder.Entity("People").Property("Id"); - }, - _ => { }, - builder => builder.Entity("People").Property("Name"), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal(2, table.Columns.Count); - var nameColumn = Assert.Single(table.Columns, c => c.Name == "Name"); - Assert.Equal("POSIX", nameColumn.Collation); - }); - - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"";"); - } - - [ConditionalFact] - public async Task Add_column_with_collation_overriding_default() - { - await Test( - builder => - { - builder.UseDefaultColumnCollation("POSIX"); - builder.Entity("People").Property("Id"); - }, - _ => { }, - builder => builder.Entity("People").Property("Name") - .UseCollation("C"), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal(2, table.Columns.Count); - var nameColumn = Assert.Single(table.Columns, c => c.Name == "Name"); - Assert.Equal("C", nameColumn.Collation); - }); - - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""C"";"); - } -#pragma warning restore CS0618 - public override async Task Add_column_shared() { await base.Add_column_shared(); @@ -834,8 +768,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); } [Fact] @@ -859,8 +792,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED ALWAYS AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED ALWAYS AS IDENTITY;"""); } [Fact] @@ -898,7 +830,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10);"); + """ +ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10); +"""); } [Fact] @@ -935,8 +869,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" serial NOT NULL;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" serial NOT NULL;"""); } [Fact] @@ -960,8 +893,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); } [Fact] @@ -1008,8 +940,7 @@ await Test( Assert.Equal("text", column.StoreType); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } [Fact] @@ -1031,19 +962,37 @@ await Test( Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - """ -ALTER TABLE "Blogs" ADD "Title" text COMPRESSION pglz; -"""); + AssertSql("""ALTER TABLE "Blogs" ADD "Title" text COMPRESSION pglz;"""); } public override async Task Alter_column_change_type() { await base.Alter_column_change_type(); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE bigint;"""); + } + + [Fact] + public virtual async Task Alter_column_change_type_preserves_collation() + { + await Test( + builder => builder.Entity("People").Property("Id"), + builder => builder.Entity("People").Property("SomeColumn") + .HasColumnType("varchar") + .UseCollation(NonDefaultCollation), + builder => builder.Entity("People").Property("SomeColumn") + .HasColumnType("text") + .UseCollation(NonDefaultCollation), + model => + { + var table = Assert.Single(model.Tables); + var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); + Assert.Equal(NonDefaultCollation, column.Collation); + }); + AssertSql( """ -ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE bigint; +ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE text COLLATE "POSIX"; """); } @@ -1113,9 +1062,9 @@ public override async Task Alter_column_make_computed(bool? stored) await base.Alter_column_make_computed(stored); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" + ""Y"") STORED NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" + "Y") STORED NOT NULL;"""); } public override async Task Alter_column_change_computed() @@ -1150,9 +1099,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" - ""Y"") STORED NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" - "Y") STORED NOT NULL;"""); } public override async Task Alter_column_change_computed_recreates_indexes() @@ -1195,11 +1144,11 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" - ""Y"") STORED NOT NULL;", + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" - "Y") STORED NOT NULL;""", // - @"CREATE INDEX ""IX_People_Sum"" ON ""People"" (""Sum"");"); + """CREATE INDEX "IX_People_Sum" ON "People" ("Sum");"""); } public override Task Alter_column_change_computed_type() @@ -1222,7 +1171,9 @@ await Test( e.Property("Y"); }), builder => builder.Entity("People").Property("Sum") - .HasComputedColumnSql(@"""X"" + ""Y""", stored: true), + .HasComputedColumnSql(""" + "X" + "Y" + """, stored: true), builder => builder.Entity("People").Property("Sum"), model => { @@ -1233,17 +1184,16 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer NOT NULL;"""); } public override async Task Alter_column_add_comment() { await base.Alter_column_add_comment(); - AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS 'Some comment';"); + AssertSql("""COMMENT ON COLUMN "People"."Id" IS 'Some comment';"""); } public override async Task Alter_computed_column_add_comment() @@ -1273,16 +1223,14 @@ await Test( } }); - AssertSql( - @"COMMENT ON COLUMN ""People"".""SomeColumn"" IS 'Some comment';"); + AssertSql("""COMMENT ON COLUMN "People"."SomeColumn" IS 'Some comment';"""); } public override async Task Alter_column_change_comment() { await base.Alter_column_change_comment(); - AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS 'Some comment2';"); + AssertSql("""COMMENT ON COLUMN "People"."Id" IS 'Some comment2';"""); } public override async Task Alter_column_remove_comment() @@ -1290,7 +1238,7 @@ public override async Task Alter_column_remove_comment() await base.Alter_column_remove_comment(); AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS NULL;"); + """COMMENT ON COLUMN "People"."Id" IS NULL;"""); } [Fact] @@ -1337,8 +1285,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET GENERATED ALWAYS;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" SET GENERATED ALWAYS;"""); } [Fact] @@ -1519,10 +1466,14 @@ await Test( }); AssertSql( - @"CREATE SEQUENCE ""People_Id_seq"" AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;", + """ +CREATE SEQUENCE "People_Id_seq" AS integer START WITH 1 INCREMENT BY 1 NO CYCLE; +""", // - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET DEFAULT (nextval('""People_Id_seq""')); -ALTER SEQUENCE ""People_Id_seq"" OWNED BY ""People"".""Id"";"); + """ +ALTER TABLE "People" ALTER COLUMN "Id" SET DEFAULT (nextval('"People_Id_seq"')); +ALTER SEQUENCE "People_Id_seq" OWNED BY "People"."Id"; +"""); } [Fact] @@ -1545,10 +1496,14 @@ await Test( }); AssertSql( - @"CREATE SEQUENCE some_schema.""People_Id_seq"" AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;", + """ +CREATE SEQUENCE some_schema."People_Id_seq" AS integer START WITH 1 INCREMENT BY 1 NO CYCLE; +""", // - @"ALTER TABLE some_schema.""People"" ALTER COLUMN ""Id"" SET DEFAULT (nextval('some_schema.""People_Id_seq""')); -ALTER SEQUENCE some_schema.""People_Id_seq"" OWNED BY some_schema.""People"".""Id"";"); + """ +ALTER TABLE some_schema."People" ALTER COLUMN "Id" SET DEFAULT (nextval('some_schema."People_Id_seq"')); +ALTER SEQUENCE some_schema."People_Id_seq" OWNED BY some_schema."People"."Id"; +"""); } [Fact] @@ -1571,10 +1526,14 @@ await Test( }); AssertSql( - @"CREATE SEQUENCE ""People_Id_seq"" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;", + """ +CREATE SEQUENCE "People_Id_seq" START WITH 1 INCREMENT BY 1 NO CYCLE; +""", // - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET DEFAULT (nextval('""People_Id_seq""')); -ALTER SEQUENCE ""People_Id_seq"" OWNED BY ""People"".""Id"";"); + """ +ALTER TABLE "People" ALTER COLUMN "Id" SET DEFAULT (nextval('"People_Id_seq"')); +ALTER SEQUENCE "People_Id_seq" OWNED BY "People"."Id"; +"""); } [Fact] @@ -1598,7 +1557,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET GENERATED ALWAYS;"); + """ALTER TABLE "People" ALTER COLUMN "Id" SET GENERATED ALWAYS;"""); } [Fact] @@ -1656,8 +1615,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" TYPE bigint;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" TYPE bigint;"""); } [Fact] @@ -1681,8 +1639,7 @@ await Test( Assert.Equal(10, options.StartValue); // Restarting doesn't change the scaffolded start value }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" RESTART WITH 20;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" RESTART WITH 20;"""); } [Fact] @@ -1690,8 +1647,7 @@ public override async Task Alter_column_set_collation() { await base.Alter_column_set_collation(); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name" TYPE text COLLATE "POSIX";"""); } [Fact] @@ -1699,35 +1655,32 @@ public override async Task Alter_column_reset_collation() { await base.Alter_column_reset_collation(); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""default"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name" TYPE text COLLATE "default";"""); } -#pragma warning disable CS0618 - [Fact] - public async Task Alter_column_change_default_column_collation() + public override async Task Convert_string_column_to_a_json_column_containing_reference() { - await Test( - builder => builder.Entity( - "People", b => - { - b.Property("Id"); - b.Property("Name"); - }), - builder => builder.UseDefaultColumnCollation("POSIX"), - builder => builder.UseDefaultColumnCollation("C"), - _ => - { - // var table = Assert.Single(model.Tables); - // Assert.Equal(2, table.Columns.Count); - // var nameColumn = Assert.Single(table.Columns, c => c.Name == "Name"); - // Assert.Equal("C", nameColumn.Collation); - }); + var exception = + await Assert.ThrowsAsync(() => base.Convert_string_column_to_a_json_column_containing_reference()); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""C"";"); + Assert.Equal("42804", exception.SqlState); // column "Name" cannot be cast automatically to type jsonb + } + + public override async Task Convert_string_column_to_a_json_column_containing_required_reference() + { + var exception = + await Assert.ThrowsAsync(() => base.Convert_string_column_to_a_json_column_containing_required_reference()); + + Assert.Equal("42804", exception.SqlState); // column "Name" cannot be cast automatically to type jsonb + } + + public override async Task Convert_string_column_to_a_json_column_containing_collection() + { + var exception = + await Assert.ThrowsAsync(() => base.Convert_string_column_to_a_json_column_containing_collection()); + + Assert.Equal("42804", exception.SqlState); // column "Name" cannot be cast automatically to type jsonb } -#pragma warning restore CS0618 [Fact] public virtual async Task Alter_column_computed_set_collation() @@ -1742,7 +1695,9 @@ await Test( "People", b => { b.Property("Name"); - b.Property("Name2").HasComputedColumnSql(@"""Name""", stored: true); + b.Property("Name2").HasComputedColumnSql(""" + "Name" + """, stored: true); }), _ => { }, builder => builder.Entity("People").Property("Name2") @@ -1750,12 +1705,13 @@ await Test( model => { var computedColumn = Assert.Single(Assert.Single(model.Tables).Columns, c => c.Name == "Name2"); - Assert.Equal(@"""Name""", computedColumn.ComputedColumnSql); + Assert.Equal(""" + "Name" + """, computedColumn.ComputedColumnSql); Assert.Equal(NonDefaultCollation, computedColumn.Collation); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name2"" TYPE text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name2" TYPE text COLLATE "POSIX";"""); } [Fact] @@ -1777,8 +1733,7 @@ await Test( Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ALTER COLUMN ""Title"" SET COMPRESSION pglz"); + AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION pglz"""); } [Fact] @@ -1800,16 +1755,14 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ALTER COLUMN ""Title"" SET COMPRESSION default"); + AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION default"""); } public override async Task Drop_column() { await base.Drop_column(); - AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""SomeColumn"";"); + AssertSql("""ALTER TABLE "People" DROP COLUMN "SomeColumn";"""); } public override async Task Drop_column_primary_key() @@ -1817,9 +1770,9 @@ public override async Task Drop_column_primary_key() await base.Drop_column_primary_key(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" DROP COLUMN ""Id"";"); + """ALTER TABLE "People" DROP COLUMN "Id";"""); } public override async Task Drop_column_computed_and_non_computed_with_dependency() @@ -1845,9 +1798,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Y"";", + """ALTER TABLE "People" DROP COLUMN "Y";""", // - @"ALTER TABLE ""People"" DROP COLUMN ""X"";"); + """ALTER TABLE "People" DROP COLUMN "X";"""); } [Fact] @@ -1883,8 +1836,7 @@ public override async Task Rename_column() { await base.Rename_column(); - AssertSql( - @"ALTER TABLE ""People"" RENAME COLUMN ""SomeColumn"" TO ""SomeOtherColumn"";"); + AssertSql("""ALTER TABLE "People" RENAME COLUMN "SomeColumn" TO "SomeOtherColumn";"""); } #endregion @@ -1895,55 +1847,23 @@ public override async Task Create_index_unique() { await base.Create_index_unique(); - AssertSql( - @"CREATE UNIQUE INDEX ""IX_People_FirstName_LastName"" ON ""People"" (""FirstName"", ""LastName"");"); + AssertSql("""CREATE UNIQUE INDEX "IX_People_FirstName_LastName" ON "People" ("FirstName", "LastName");"""); } public override async Task Create_index_descending() { await base.Create_index_descending(); - AssertSql( - @"CREATE INDEX ""IX_People_X"" ON ""People"" (""X"" DESC);"); + AssertSql("""CREATE INDEX "IX_People_X" ON "People" ("X" DESC);"""); } public override async Task Create_index_descending_mixed() { await base.Create_index_descending_mixed(); - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + AssertSql("""CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } -#pragma warning disable CS0618 // HasSortOrder is obsolete - [Fact] - public virtual async Task Create_index_descending_mixed_legacy_api() - { - await Test( - builder => builder.Entity( - "People", e => - { - e.Property("Id"); - e.Property("X"); - e.Property("Y"); - e.Property("Z"); - }), - builder => { }, - builder => builder.Entity("People") - .HasIndex("X", "Y", "Z") - .HasSortOrder(SortOrder.Ascending, SortOrder.Descending), - model => - { - var table = Assert.Single(model.Tables); - var index = Assert.Single(table.Indexes); - Assert.Collection(index.IsDescending, Assert.False, Assert.True, Assert.False); - }); - - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); - } -#pragma warning restore CS0618 - [Fact] public virtual async Task Create_index_descending_mixed_legacy_annotation() { @@ -1967,24 +1887,21 @@ await Test( Assert.Collection(index.IsDescending, Assert.False, Assert.True, Assert.False); }); - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + AssertSql("""CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } public override async Task Create_index_with_filter() { await base.Create_index_with_filter(); - AssertSql( - @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } public override async Task Create_unique_index_with_filter() { await base.Create_unique_index_with_filter(); - AssertSql( - @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL AND ""Name"" <> '';"); + AssertSql("""CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL AND "Name" <> '';"""); } [Fact] @@ -2026,8 +1943,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", last_name);" - : @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"");"); + ? """CREATE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", last_name);""" + : """CREATE INDEX "IX_People_Name" ON "People" ("Name");"""); } [Fact] @@ -2045,12 +1962,14 @@ await Test( _ => { }, builder => builder.Entity("People").HasIndex("Name") .IncludeProperties("FirstName", "LastName") - .HasFilter(@"""Name"" IS NOT NULL"), + .HasFilter(""" + "Name" IS NOT NULL + """), model => { var table = Assert.Single(model.Tables); var index = Assert.Single(table.Indexes); - Assert.Equal(@"(""Name"" IS NOT NULL)", index.Filter); + Assert.Equal("""("Name" IS NOT NULL)""", index.Filter); Assert.Single(index.Columns); Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); @@ -2071,8 +1990,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"") WHERE ""Name"" IS NOT NULL;" - : @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + ? """CREATE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName") WHERE "Name" IS NOT NULL;""" + : """CREATE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } [Fact] @@ -2116,8 +2035,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"");" - : @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"");"); + ? """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName");""" + : """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name");"""); } [Fact] @@ -2136,13 +2055,15 @@ await Test( builder => builder.Entity("People").HasIndex("Name") .IsUnique() .IncludeProperties("FirstName", "LastName") - .HasFilter(@"""Name"" IS NOT NULL"), + .HasFilter(""" + "Name" IS NOT NULL + """), model => { var table = Assert.Single(model.Tables); var index = Assert.Single(table.Indexes); Assert.True(index.IsUnique); - Assert.Equal(@"(""Name"" IS NOT NULL)", index.Filter); + Assert.Equal("""("Name" IS NOT NULL)""", index.Filter); Assert.Single(index.Columns); Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); @@ -2163,8 +2084,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"") WHERE ""Name"" IS NOT NULL;" - : @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + ? """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName") WHERE "Name" IS NOT NULL;""" + : """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } [Fact] @@ -2182,8 +2103,7 @@ await Test( .IsCreatedConcurrently(), asserter: null); // No scaffolding for IsCreatedConcurrently - AssertSql( - @"CREATE INDEX CONCURRENTLY ""IX_People_Age"" ON ""People"" (""Age"");"); + AssertSql("""CREATE INDEX CONCURRENTLY "IX_People_Age" ON "People" ("Age");"""); } [Fact] @@ -2206,8 +2126,7 @@ await Test( Assert.Equal("hash", index[NpgsqlAnnotationNames.IndexMethod]); }); - AssertSql( - @"CREATE INDEX ""IX_People_Age"" ON ""People"" USING hash (""Age"");"); + AssertSql("""CREATE INDEX "IX_People_Age" ON "People" USING hash ("Age");"""); } [Fact] @@ -2231,13 +2150,53 @@ await Test( Assert.Equal(new[] { "text_pattern_ops", null }, index[NpgsqlAnnotationNames.IndexOperators]); }); - AssertSql( - @"CREATE INDEX ""IX_People_FirstName_LastName"" ON ""People"" (""FirstName"" text_pattern_ops, ""LastName"");"); + AssertSql("""CREATE INDEX "IX_People_FirstName_LastName" ON "People" ("FirstName" text_pattern_ops, "LastName");"""); } - // Index collation: which collations are available on a given PostgreSQL varies (e.g. Linux vs. Windows), - // so we test support for this on the generated SQL only, in NpgsqlMigrationSqlGeneratorTest, and not against - // the database here. + [Fact] + public virtual async Task Create_index_with_collation() + { + await Test( + builder => + { + builder.Entity("People", e => e.Property("Name")); + builder.HasCollation("some_collation", locale: "POSIX", provider: "libc"); + }, + _ => { }, + builder => builder.Entity("People").HasIndex("Name").UseCollation("some_collation"), + model => + { + var table = Assert.Single(model.Tables); + var index = Assert.Single(table.Indexes); + Assert.Equal("some_collation", Assert.Single((IReadOnlyList)index[RelationalAnnotationNames.Collation]!)); + }); + + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation);"""); + } + + [Fact] // #3027 + public virtual async Task Create_index_with_collation_and_operators() + { + await Test( + builder => + { + builder.Entity("People", e => e.Property("Name")); + builder.HasCollation("some_collation", locale: "POSIX", provider: "libc"); + }, + _ => { }, + builder => builder.Entity("People").HasIndex("Name") + .UseCollation("some_collation") + .HasOperators("text_pattern_ops"), + model => + { + var table = Assert.Single(model.Tables); + var index = Assert.Single(table.Indexes); + Assert.Equal("text_pattern_ops", Assert.Single((IReadOnlyList)index[NpgsqlAnnotationNames.IndexOperators]!)); + Assert.Equal("some_collation", Assert.Single((IReadOnlyList)index[RelationalAnnotationNames.Collation]!)); + }); + + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation text_pattern_ops);"""); + } [Fact] public virtual async Task Create_index_with_null_sort_order() @@ -2264,7 +2223,7 @@ await Test( }); AssertSql( - @"CREATE INDEX ""IX_People_FirstName_MiddleName_LastName"" ON ""People"" (""FirstName"" NULLS FIRST, ""MiddleName"", ""LastName"" NULLS LAST);"); + """CREATE INDEX "IX_People_FirstName_MiddleName_LastName" ON "People" ("FirstName" NULLS FIRST, "MiddleName", "LastName" NULLS LAST);"""); } [Fact] @@ -2284,7 +2243,7 @@ await Test( _ => { }); AssertSql( - @"CREATE INDEX ""IX_Blogs_Title_Description"" ON ""Blogs"" (to_tsvector('simple', ""Title"" || ' ' || coalesce(""Description"", '')));"); + """CREATE INDEX "IX_Blogs_Title_Description" ON "Blogs" (to_tsvector('simple', "Title" || ' ' || coalesce("Description", '')));"""); } [Fact] @@ -2305,7 +2264,7 @@ await Test( _ => { }); AssertSql( - @"CREATE INDEX ""IX_Blogs_Title_Description"" ON ""Blogs"" USING GIN (to_tsvector('simple', ""Title"" || ' ' || coalesce(""Description"", '')));"); + """CREATE INDEX "IX_Blogs_Title_Description" ON "Blogs" USING GIN (to_tsvector('simple', "Title" || ' ' || coalesce("Description", '')));"""); } [ConditionalFact] @@ -2323,10 +2282,10 @@ await Test( builder => builder.Entity( "People", b => { - b.HasIndex(new[] { "Age" }, "IX_NullsDistinct") + b.HasIndex(["Age"], "IX_NullsDistinct") .IsUnique(); - b.HasIndex(new[] { "Age" }, "IX_NullsNotDistinct") + b.HasIndex(["Age"], "IX_NullsNotDistinct") .IsUnique() .AreNullsDistinct(false); }), @@ -2343,13 +2302,9 @@ await Test( }); AssertSql( - """ -CREATE UNIQUE INDEX "IX_NullsDistinct" ON "People" ("Age"); -""", + """CREATE UNIQUE INDEX "IX_NullsDistinct" ON "People" ("Age");""", // - """ -CREATE UNIQUE INDEX "IX_NullsNotDistinct" ON "People" ("Age") NULLS NOT DISTINCT; -"""); + """CREATE UNIQUE INDEX "IX_NullsNotDistinct" ON "People" ("Age") NULLS NOT DISTINCT;"""); } [Fact] @@ -2378,8 +2333,7 @@ await Test( Assert.Equal("70", storageParameter.Value); }); - AssertSql( - @"CREATE INDEX ""IX_People_Age"" ON ""People"" (""Age"") WITH (fillfactor=70);"); + AssertSql("""CREATE INDEX "IX_People_Age" ON "People" ("Age") WITH (fillfactor=70);"""); } public override async Task Alter_index_change_sort_order() @@ -2387,25 +2341,23 @@ public override async Task Alter_index_change_sort_order() await base.Alter_index_change_sort_order(); AssertSql( - @"DROP INDEX ""IX_People_X_Y_Z"";", + """DROP INDEX "IX_People_X_Y_Z";""", // - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + """CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } public override async Task Drop_index() { await base.Drop_index(); - AssertSql( - @"DROP INDEX ""IX_People_SomeField"";"); + AssertSql("""DROP INDEX "IX_People_SomeField";"""); } public override async Task Rename_index() { await base.Rename_index(); - AssertSql( - @"ALTER INDEX ""Foo"" RENAME TO foo;"); + AssertSql("""ALTER INDEX "Foo" RENAME TO foo;"""); } #endregion @@ -2422,14 +2374,16 @@ public override async Task Add_primary_key_int() ALTER TABLE "People" ALTER COLUMN "SomeField" ADD GENERATED BY DEFAULT AS IDENTITY; """, // - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_People"" PRIMARY KEY (""SomeField"");"); + """ +ALTER TABLE "People" ADD CONSTRAINT "PK_People" PRIMARY KEY ("SomeField"); +"""); } public override async Task Add_primary_key_string() { await base.Add_primary_key_string(); - AssertSql(@"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_People"" PRIMARY KEY (""SomeField"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "PK_People" PRIMARY KEY ("SomeField");"""); } public override async Task Add_primary_key_with_name() @@ -2443,15 +2397,16 @@ public override async Task Add_primary_key_with_name() ALTER TABLE "People" ALTER COLUMN "SomeField" SET DEFAULT ''; """, // - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_Foo"" PRIMARY KEY (""SomeField"");"); + """ +ALTER TABLE "People" ADD CONSTRAINT "PK_Foo" PRIMARY KEY ("SomeField"); +"""); } public override async Task Add_primary_key_composite_with_name() { await base.Add_primary_key_composite_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_Foo"" PRIMARY KEY (""SomeField1"", ""SomeField2"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "PK_Foo" PRIMARY KEY ("SomeField1", "SomeField2");"""); } public override async Task Drop_primary_key_int() @@ -2459,17 +2414,16 @@ public override async Task Drop_primary_key_int() await base.Drop_primary_key_int(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" ALTER COLUMN ""SomeField"" DROP IDENTITY;"); + """ALTER TABLE "People" ALTER COLUMN "SomeField" DROP IDENTITY;"""); } public override async Task Drop_primary_key_string() { await base.Drop_primary_key_string(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "PK_People";"""); } public override Task Add_foreign_key() @@ -2480,9 +2434,9 @@ public override async Task Add_foreign_key_with_name() await base.Add_foreign_key_with_name(); AssertSql( - @"CREATE INDEX ""IX_Orders_CustomerId"" ON ""Orders"" (""CustomerId"");", + """CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");""", // - @"ALTER TABLE ""Orders"" ADD CONSTRAINT ""FK_Foo"" FOREIGN KEY (""CustomerId"") REFERENCES ""Customers"" (""Id"") ON DELETE CASCADE;"); + """ALTER TABLE "Orders" ADD CONSTRAINT "FK_Foo" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id") ON DELETE CASCADE;"""); } public override async Task Drop_foreign_key() @@ -2490,49 +2444,44 @@ public override async Task Drop_foreign_key() await base.Drop_foreign_key(); AssertSql( - @"ALTER TABLE ""Orders"" DROP CONSTRAINT ""FK_Orders_Customers_CustomerId"";", + """ALTER TABLE "Orders" DROP CONSTRAINT "FK_Orders_Customers_CustomerId";""", // - @"DROP INDEX ""IX_Orders_CustomerId"";"); + """DROP INDEX "IX_Orders_CustomerId";"""); } public override async Task Add_unique_constraint() { await base.Add_unique_constraint(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""AK_People_AlternateKeyColumn"" UNIQUE (""AlternateKeyColumn"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "AK_People_AlternateKeyColumn" UNIQUE ("AlternateKeyColumn");"""); } public override async Task Add_unique_constraint_composite_with_name() { await base.Add_unique_constraint_composite_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""AK_Foo"" UNIQUE (""AlternateKeyColumn1"", ""AlternateKeyColumn2"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "AK_Foo" UNIQUE ("AlternateKeyColumn1", "AlternateKeyColumn2");"""); } public override async Task Drop_unique_constraint() { await base.Drop_unique_constraint(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""AK_People_AlternateKeyColumn"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "AK_People_AlternateKeyColumn";"""); } public override async Task Add_check_constraint_with_name() { await base.Add_check_constraint_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""CK_People_Foo"" CHECK (""DriverLicense"" > 0);"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "CK_People_Foo" CHECK ("DriverLicense" > 0);"""); } public override async Task Drop_check_constraint() { await base.Drop_check_constraint(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""CK_People_Foo"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "CK_People_Foo";"""); } #endregion @@ -2543,8 +2492,7 @@ public override async Task Create_sequence() { await base.Create_sequence(); - AssertSql( - @"CREATE SEQUENCE ""TestSequence"" AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;"); + AssertSql("""CREATE SEQUENCE "TestSequence" AS integer START WITH 1 INCREMENT BY 1 NO CYCLE;"""); } public override async Task Create_sequence_all_settings() @@ -2561,7 +2509,9 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'dbo2') THEN END $EF$; """, // - @"CREATE SEQUENCE dbo2.""TestSequence"" START WITH 3 INCREMENT BY 2 MINVALUE 2 MAXVALUE 916 CYCLE;"); + """ +CREATE SEQUENCE dbo2."TestSequence" START WITH 3 INCREMENT BY 2 MINVALUE 2 MAXVALUE 916 CYCLE; +"""); } [Fact] @@ -2577,31 +2527,13 @@ await Test( Assert.Equal("smallint", sequence.StoreType); }); - AssertSql( - @"CREATE SEQUENCE ""TestSequence"" AS smallint START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;"); + AssertSql("""CREATE SEQUENCE "TestSequence" AS smallint START WITH 1 INCREMENT BY 1 NO CYCLE;"""); } [Fact] public override async Task Alter_sequence_all_settings() { - await Test( - builder => builder.HasSequence("foo"), - _ => { }, - builder => builder.HasSequence("foo") - .StartsAt(-3) - .IncrementsBy(2) - .HasMin(-5) - .HasMax(10) - .IsCyclic(), - model => - { - var sequence = Assert.Single(model.Sequences); - Assert.Equal(-3, sequence.StartValue); - Assert.Equal(2, sequence.IncrementBy); - Assert.Equal(-5, sequence.MinValue); - Assert.Equal(10, sequence.MaxValue); - Assert.True(sequence.IsCyclic); - }); + await base.Alter_sequence_all_settings(); AssertSql( """ @@ -2619,7 +2551,9 @@ public override async Task Alter_sequence_increment_by() await base.Alter_sequence_increment_by(); AssertSql( - @"ALTER SEQUENCE foo INCREMENT BY 2 NO MINVALUE NO MAXVALUE NO CYCLE;"); + """ +ALTER SEQUENCE foo INCREMENT BY 2 NO MINVALUE NO MAXVALUE NO CYCLE; +"""); } public override async Task Alter_sequence_restart_with() @@ -2637,16 +2571,14 @@ public override async Task Drop_sequence() { await base.Drop_sequence(); - AssertSql( - @"DROP SEQUENCE ""TestSequence"";"); + AssertSql("""DROP SEQUENCE "TestSequence";"""); } public override async Task Rename_sequence() { await base.Rename_sequence(); - AssertSql( - @"ALTER SEQUENCE ""TestSequence"" RENAME TO testsequence;"); + AssertSql("""ALTER SEQUENCE "TestSequence" RENAME TO testsequence;"""); } public override async Task Move_sequence() @@ -2663,7 +2595,9 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'TestSequenceSchema') T END $EF$; """, // - @"ALTER SEQUENCE ""TestSequence"" SET SCHEMA ""TestSequenceSchema"";"); + """ +ALTER SEQUENCE "TestSequence" SET SCHEMA "TestSequenceSchema"; +"""); } #endregion @@ -2817,7 +2751,15 @@ SELECT setval( """); } - #endregion + + #endregion Data seeding + + public override async Task Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table_core("ARRAY[3, 2, 1]"); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT (ARRAY[3, 2, 1]);"""); + } #region PostgreSQL extensions @@ -2834,8 +2776,7 @@ await Test( Assert.Equal("public", citext.Schema); }); - AssertSql( - @"CREATE EXTENSION IF NOT EXISTS citext;"); + AssertSql("CREATE EXTENSION IF NOT EXISTS citext;"); } [Fact] @@ -2852,12 +2793,14 @@ await Test( }); AssertSql( - @"DO $EF$ -BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN - CREATE SCHEMA some_schema; - END IF; -END $EF$;", + """ + DO $EF$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN + CREATE SCHEMA some_schema; + END IF; + END $EF$; + """, // @"CREATE EXTENSION IF NOT EXISTS citext SCHEMA some_schema;"); } @@ -2871,7 +2814,7 @@ public virtual async Task Create_enum() { await Test( _ => { }, - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), model => { var moodEnum = Assert.Single(model.GetPostgresEnums()); @@ -2883,8 +2826,7 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql( - @"CREATE TYPE ""Mood"" AS ENUM ('Happy', 'Sad');"); + AssertSql("""CREATE TYPE "Mood" AS ENUM ('Happy', 'Sad');"""); } [Fact] @@ -2892,7 +2834,7 @@ public virtual async Task Create_enum_with_schema() { await Test( _ => { }, - builder => builder.HasPostgresEnum("some_schema", "Mood", new[] { "Happy", "Sad" }), + builder => builder.HasPostgresEnum("some_schema", "Mood", ["Happy", "Sad"]), model => { var moodEnum = Assert.Single(model.GetPostgresEnums()); @@ -2914,40 +2856,40 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN END $EF$; """, // - @"CREATE TYPE some_schema.""Mood"" AS ENUM ('Happy', 'Sad');"); + """ +CREATE TYPE some_schema."Mood" AS ENUM ('Happy', 'Sad'); +"""); } [Fact] public virtual async Task Drop_enum() { await Test( - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), _ => { }, model => Assert.Empty(model.GetPostgresEnums())); - AssertSql( - @"DROP TYPE ""Mood"";"); + AssertSql("""DROP TYPE "Mood";"""); } [Fact] // #979 public virtual async Task Do_not_alter_existing_enum_when_creating_new_one() { await Test( - builder => builder.HasPostgresEnum("Enum1", new[] { "A", "B" }), + builder => builder.HasPostgresEnum("Enum1", ["A", "B"]), _ => { }, - builder => builder.HasPostgresEnum("Enum2", new[] { "X", "Y" }), + builder => builder.HasPostgresEnum("Enum2", ["X", "Y"]), model => Assert.Equal(2, model.GetPostgresEnums().Count())); - AssertSql( - @"CREATE TYPE ""Enum2"" AS ENUM ('X', 'Y');"); + AssertSql("""CREATE TYPE "Enum2" AS ENUM ('X', 'Y');"""); } [Fact] public virtual async Task Alter_enum_add_label_at_end() { await Test( - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad", "Angry" }), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad", "Angry"]), model => { var moodEnum = Assert.Single(model.GetPostgresEnums()); @@ -2958,16 +2900,15 @@ await Test( l => Assert.Equal("Angry", l)); }); - AssertSql( - @"ALTER TYPE ""Mood"" ADD VALUE 'Angry';"); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry';"""); } [Fact] public virtual async Task Alter_enum_add_label_in_middle() { await Test( - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Angry", "Sad" }), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Angry", "Sad"]), model => { var moodEnum = Assert.Single(model.GetPostgresEnums()); @@ -2978,21 +2919,38 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql( - @"ALTER TYPE ""Mood"" ADD VALUE 'Angry' BEFORE 'Sad';"); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' AFTER 'Happy';"""); + } + + [Fact] + public virtual async Task Alter_enum_change_label_ordering_does_nothing() + { + await Test( + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Sad", "Happy"]), + model => + { + var moodEnum = Assert.Single(model.GetPostgresEnums()); + Assert.Collection( + moodEnum.Labels, + l => Assert.Equal("Happy", l), + l => Assert.Equal("Sad", l)); + }); + + AssertSql(); } [Fact] public virtual Task Alter_enum_drop_label_not_supported() => TestThrows( - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), - builder => builder.HasPostgresEnum("Mood", new[] { "Happy" })); + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Happy"])); [Fact] public virtual Task Alter_enum_change_label_not_supported() => TestThrows( - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Sad" }), - builder => builder.HasPostgresEnum("Mood", new[] { "Happy", "Angry" })); + builder => builder.HasPostgresEnum("Mood", ["Happy", "Sad"]), + builder => builder.HasPostgresEnum("Mood", ["Happy", "Angry"])); #endregion @@ -3058,8 +3016,7 @@ await Test( _ => { }, model => Assert.Empty(PostgresCollation.GetCollations(model))); - AssertSql( - @"DROP COLLATION dummy;"); + AssertSql("""DROP COLLATION dummy;"""); } [Fact] @@ -3091,8 +3048,7 @@ await Test( Assert.Equal("tsvector", column.StoreType); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""TextColumn"")) STORED;"); + AssertSql("""ALTER TABLE "Blogs" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (to_tsvector('english', "TextColumn")) STORED;"""); } [Fact] @@ -3116,7 +3072,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (jsonb_to_tsvector('english', ""JsonbColumn"", '""all""')) STORED;"); + """ALTER TABLE "People" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (jsonb_to_tsvector('english', "JsonbColumn", '"all"')) STORED;"""); } [Fact] @@ -3147,7 +3103,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""RequiredTextColumn"" || ' ' || coalesce(""OptionalTextColumn"", '')) || jsonb_to_tsvector('english', ""RequiredJsonbColumn"", '""all""') || json_to_tsvector('english', coalesce(""OptionalJsonColumn"", '{}'), '""all""')) STORED;"); + """ALTER TABLE "People" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (to_tsvector('english', "RequiredTextColumn" || ' ' || coalesce("OptionalTextColumn", '')) || jsonb_to_tsvector('english', "RequiredJsonbColumn", '"all"') || json_to_tsvector('english', coalesce("OptionalJsonColumn", '{}'), '"all"')) STORED;"""); } [Fact] @@ -3175,18 +3131,143 @@ await Test( var column = Assert.Single(table.Columns, c => c.Name == "TsVector"); Assert.Equal("tsvector", column.StoreType); Assert.Equal( - @"to_tsvector('english'::regconfig, ((""Title"" || ' '::text) || COALESCE(""Description"", ''::text)))", + """to_tsvector('english'::regconfig, (("Title" || ' '::text) || COALESCE("Description", ''::text)))""", column.ComputedColumnSql); }); AssertSql( - @"ALTER TABLE ""Blogs"" DROP COLUMN ""TsVector"";", + """ALTER TABLE "Blogs" DROP COLUMN "TsVector";""", // - @"ALTER TABLE ""Blogs"" ADD ""TsVector"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""Title"" || ' ' || coalesce(""Description"", ''))) STORED;"); + """ALTER TABLE "Blogs" ADD "TsVector" tsvector GENERATED ALWAYS AS (to_tsvector('english', "Title" || ' ' || coalesce("Description", ''))) STORED;"""); } #endregion PostgreSQL full-text search + public override async Task Add_required_primitive_collection_to_existing_table() + { + await base.Add_required_primitive_collection_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL;"""); + } + + public override async Task Add_required_primitive_collection_with_custom_default_value_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_default_value_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT ARRAY[1,2,3]::integer[];"""); + } + + public override async Task Add_required_primitive_collection_with_custom_default_value_sql_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_default_value_sql_to_existing_table_core("ARRAY[1,2,3]"); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT (ARRAY[1,2,3]);"""); + } + + [ConditionalFact(Skip = "issue #33038")] + public override async Task Add_required_primitive_collection_with_custom_converter_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_converter_to_existing_table(); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'nothing'; +"""); + } + + public override async Task Add_required_primitive_collection_with_custom_converter_and_custom_default_value_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_converter_and_custom_default_value_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" text NOT NULL DEFAULT 'some numbers';"""); + } + + public override async Task Add_optional_primitive_collection_to_existing_table() + { + await base.Add_optional_primitive_collection_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[];"""); + } + + public override async Task Create_table_with_required_primitive_collection() + { + await base.Create_table_with_required_primitive_collection(); + + AssertSql( + """ +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text, + "Numbers" integer[] NOT NULL, + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +"""); + } + + public override async Task Create_table_with_optional_primitive_collection() + { + await base.Create_table_with_optional_primitive_collection(); + + AssertSql( + """ +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text, + "Numbers" integer[], + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +"""); + } + + public override async Task Create_table_with_complex_type_with_required_properties_on_derived_entity_in_TPH() + { + await base.Create_table_with_complex_type_with_required_properties_on_derived_entity_in_TPH(); + + AssertSql( + """ +CREATE TABLE "Contacts" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Discriminator" character varying(8) NOT NULL, + "Name" text, + "Number" integer, + "MyComplex_Prop" text, + "MyComplex_MyNestedComplex_Bar" timestamp with time zone, + "MyComplex_MyNestedComplex_Foo" integer, + CONSTRAINT "PK_Contacts" PRIMARY KEY ("Id") +); +"""); + } + + public override async Task Add_required_primitve_collection_to_existing_table() + { + await base.Add_required_primitve_collection_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL;"""); + } + + public override async Task Add_required_primitve_collection_with_custom_default_value_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_default_value_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT ARRAY[1,2,3]::integer[];"""); + } + + [ConditionalFact(Skip = "issue #33038")] + public override async Task Add_required_primitve_collection_with_custom_converter_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_converter_to_existing_table(); + + AssertSql("ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'nothing';"); + } + + public override async Task Add_required_primitve_collection_with_custom_converter_and_custom_default_value_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_converter_and_custom_default_value_to_existing_table(); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" text NOT NULL DEFAULT 'some numbers';"""); + } + + protected override string NonDefaultCollation => "POSIX"; @@ -3223,5 +3304,5 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build protected override ICollection GetAdditionalReferences() => AdditionalReferences; - private static readonly BuildReference[] AdditionalReferences = { BuildReference.ByName("Npgsql") }; + private static readonly BuildReference[] AdditionalReferences = [BuildReference.ByName("Npgsql")]; } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs index cfbff2778..0b3dc8292 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs @@ -7,7 +7,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; -public class NpgsqlMigrationsSqlGeneratorTest : MigrationsSqlGeneratorTestBase +public class NpgsqlMigrationsSqlGeneratorTest() : MigrationsSqlGeneratorTestBase( + NpgsqlTestHelpers.Instance, + new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite(), + NpgsqlTestHelpers.Instance.AddProviderOptions( + ((IRelationalDbContextOptionsBuilderInfrastructure) + new NpgsqlDbContextOptionsBuilder(new DbContextOptionsBuilder()).UseNetTopologySuite()) + .OptionsBuilder).Options) { #region Database @@ -17,8 +23,10 @@ public virtual void CreateDatabaseOperation() Generate(new NpgsqlCreateDatabaseOperation { Name = "Northwind" }); AssertSql( - @"CREATE DATABASE ""Northwind""; -"); + """ +CREATE DATABASE "Northwind"; + +"""); } [ConditionalFact] @@ -473,26 +481,6 @@ public override void Sequence_restart_operation(long? startsAt) : """ALTER SEQUENCE dbo."TestRestartSequenceOperation" RESTART;"""); } - // Which index collations are available on a given PostgreSQL varies (e.g. Linux vs. Windows) - // so we test support for this on the generated SQL only, and not against the database in MigrationsNpgsqlTest. - [Fact] - public void CreateIndexOperation_collation() - { - Generate( - new CreateIndexOperation - { - Name = "IX_People_Name", - Table = "People", - Schema = "dbo", - Columns = new[] { "FirstName", "LastName" }, - [RelationalAnnotationNames.Collation] = new[] { null, "de_DE" } - }); - - AssertSql( - @"CREATE INDEX ""IX_People_Name"" ON dbo.""People"" (""FirstName"", ""LastName"" COLLATE ""de_DE""); -"); - } - [Theory] [InlineData(MigrationsSqlGenerationOptions.Default)] [InlineData(MigrationsSqlGenerationOptions.Idempotent)] @@ -504,8 +492,7 @@ public void Alter_column_change_serial_to_identity_idempotent(MigrationsSqlGener modelBuilder.HasAnnotation(CoreAnnotationNames.ProductVersion, "3.1.0"); modelBuilder.Entity().Property("Id").UseSerialColumn(); }, - new[] - { + [ new AlterColumnOperation { Table = "Person", @@ -522,7 +509,7 @@ public void Alter_column_change_serial_to_identity_idempotent(MigrationsSqlGener NpgsqlValueGenerationStrategy.SerialColumn, } } - }, + ], options); AssertSql( @@ -541,7 +528,7 @@ public void Create_schema_idempotent() { Generate( _ => { }, - new[] { new EnsureSchemaOperation { Name = "some_schema" } }, + [new EnsureSchemaOperation { Name = "some_schema" }], MigrationsSqlGenerationOptions.Idempotent); AssertSql( @@ -577,13 +564,13 @@ public void CreateTableOperation_with_cockroach_interleave_in_parent() IsNullable = false }, }, - PrimaryKey = new AddPrimaryKeyOperation { Columns = new[] { "Id" } } + PrimaryKey = new AddPrimaryKeyOperation { Columns = ["Id"] } }; var interleaveInParent = new CockroachDbInterleaveInParent(op); interleaveInParent.ParentTableSchema = "my_schema"; interleaveInParent.ParentTableName = "my_parent"; - interleaveInParent.InterleavePrefix = new List { "col_a", "col_b" }; + interleaveInParent.InterleavePrefix = ["col_a", "col_b"]; Generate(op); @@ -626,24 +613,13 @@ public override void InsertDataOperation_throws_for_unsupported_column_types() { Table = "People", Schema = "dbo", - Columns = new[] { "First Name" }, - ColumnTypes = new[] { "foo" }, + Columns = ["First Name"], + ColumnTypes = ["foo"], Values = new object[,] { { null } } })).Message); #pragma warning restore 618 - public NpgsqlMigrationsSqlGeneratorTest() - : base( - NpgsqlTestHelpers.Instance, - new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite(), - NpgsqlTestHelpers.Instance.AddProviderOptions( - ((IRelationalDbContextOptionsBuilderInfrastructure) - new NpgsqlDbContextOptionsBuilder(new DbContextOptionsBuilder()).UseNetTopologySuite()) - .OptionsBuilder).Options) - { - } - protected override string GetGeometryCollectionStoreType() => "GEOMETRY(GEOMETRYCOLLECTION)"; } diff --git a/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs b/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs new file mode 100644 index 000000000..9478c332d --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs @@ -0,0 +1,72 @@ +using Xunit.Sdk; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.ModelBuilding; + +#nullable enable + +public class NpgsqlModelBuilderGenericTest : NpgsqlModelBuilderTestBase +{ + public class NpgsqlGenericNonRelationship(NpgsqlModelBuilderFixture fixture) : NpgsqlNonRelationship(fixture) + { + // PostgreSQL actually does support mapping multi-dimensional arrays, so no exception is thrown as expected + protected override void Mapping_throws_for_non_ignored_three_dimensional_array() + => Assert.Throws(() => base.Mapping_throws_for_non_ignored_three_dimensional_array()); + + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericComplexType(NpgsqlModelBuilderFixture fixture) : NpgsqlComplexType(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericInheritance(NpgsqlModelBuilderFixture fixture) : NpgsqlInheritance(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericOneToMany(NpgsqlModelBuilderFixture fixture) : NpgsqlOneToMany(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericManyToOne(NpgsqlModelBuilderFixture fixture) : NpgsqlManyToOne(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericOneToOne(NpgsqlModelBuilderFixture fixture) : NpgsqlOneToOne(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericManyToMany(NpgsqlModelBuilderFixture fixture) : NpgsqlManyToMany(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class NpgsqlGenericOwnedTypes(NpgsqlModelBuilderFixture fixture) : NpgsqlOwnedTypes(fixture) + { + // PostgreSQL stored procedures do not support result columns + public override void Can_use_sproc_mapping_with_owned_reference() + => Assert.Throws(() => base.Can_use_sproc_mapping_with_owned_reference()); + + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } +} diff --git a/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs b/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs new file mode 100644 index 000000000..afce39d7a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs @@ -0,0 +1,39 @@ +#nullable enable + +using Microsoft.EntityFrameworkCore.ModelBuilding; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.ModelBuilding; + +public class NpgsqlModelBuilderTestBase : RelationalModelBuilderTest +{ + public abstract class NpgsqlNonRelationship(NpgsqlModelBuilderFixture fixture) + : RelationalNonRelationshipTestBase(fixture), IClassFixture; + + public abstract class NpgsqlComplexType(NpgsqlModelBuilderFixture fixture) + : RelationalComplexTypeTestBase(fixture), IClassFixture; + + public abstract class NpgsqlInheritance(NpgsqlModelBuilderFixture fixture) + : RelationalInheritanceTestBase(fixture), IClassFixture; + + public abstract class NpgsqlOneToMany(NpgsqlModelBuilderFixture fixture) + : RelationalOneToManyTestBase(fixture), IClassFixture; + + public abstract class NpgsqlManyToOne(NpgsqlModelBuilderFixture fixture) + : RelationalManyToOneTestBase(fixture), IClassFixture; + + public abstract class NpgsqlOneToOne(NpgsqlModelBuilderFixture fixture) + : RelationalOneToOneTestBase(fixture), IClassFixture; + + public abstract class NpgsqlManyToMany(NpgsqlModelBuilderFixture fixture) + : RelationalManyToManyTestBase(fixture), IClassFixture; + + public abstract class NpgsqlOwnedTypes(NpgsqlModelBuilderFixture fixture) + : RelationalOwnedTypesTestBase(fixture), IClassFixture; + + public class NpgsqlModelBuilderFixture : RelationalModelBuilderFixture + { + public override TestHelpers TestHelpers + => NpgsqlTestHelpers.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs index 7f7272073..433677564 100644 --- a/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class MonsterFixupChangedChangingNpgsqlTest : - MonsterFixupTestBase +public class MonsterFixupChangedChangingNpgsqlTest(MonsterFixupChangedChangingNpgsqlTest.MonsterFixupChangedChangingNpgsqlFixture fixture) + : MonsterFixupTestBase(fixture) { - public MonsterFixupChangedChangingNpgsqlTest(MonsterFixupChangedChangingNpgsqlFixture fixture) - : base(fixture) - { - } - public class MonsterFixupChangedChangingNpgsqlFixture : MonsterFixupChangedChangingFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs index a9c448cd9..45ca33212 100644 --- a/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs @@ -3,13 +3,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class MusicStoreNpgsqlTest : MusicStoreTestBase +public class MusicStoreNpgsqlTest(MusicStoreNpgsqlTest.MusicStoreNpgsqlFixture fixture) + : MusicStoreTestBase(fixture) { - public MusicStoreNpgsqlTest(MusicStoreNpgsqlFixture fixture) - : base(fixture) - { - } - public class MusicStoreNpgsqlFixture : MusicStoreFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs index 326a7d58b..5adf93326 100644 --- a/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class NotificationEntitiesNpgsqlTest - : NotificationEntitiesTestBase +public class NotificationEntitiesNpgsqlTest(NotificationEntitiesNpgsqlTest.NotificationEntitiesNpgsqlFixture fixture) + : NotificationEntitiesTestBase(fixture) { - public NotificationEntitiesNpgsqlTest(NotificationEntitiesNpgsqlFixture fixture) - : base(fixture) - { - } - public class NotificationEntitiesNpgsqlFixture : NotificationEntitiesFixtureBase { protected override string StoreName { get; } = "NotificationEntities"; diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs index 7aff5b4ca..5b331b827 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs @@ -3,13 +3,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class NpgsqlApiConsistencyTest : ApiConsistencyTestBase +public class NpgsqlApiConsistencyTest(NpgsqlApiConsistencyTest.NpgsqlApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) { - public NpgsqlApiConsistencyTest(NpgsqlApiConsistencyFixture fixture) - : base(fixture) - { - } - protected override void AddServices(ServiceCollection serviceCollection) => serviceCollection.AddEntityFrameworkNpgsql(); @@ -18,8 +14,8 @@ protected override Assembly TargetAssembly public class NpgsqlApiConsistencyFixture : ApiConsistencyFixtureBase { - public override HashSet FluentApiTypes { get; } = new() - { + public override HashSet FluentApiTypes { get; } = + [ typeof(NpgsqlDbContextOptionsBuilder), typeof(NpgsqlDbContextOptionsBuilderExtensions), typeof(NpgsqlMigrationBuilderExtensions), @@ -28,14 +24,14 @@ public class NpgsqlApiConsistencyFixture : ApiConsistencyFixtureBase typeof(NpgsqlPropertyBuilderExtensions), typeof(NpgsqlEntityTypeBuilderExtensions), typeof(NpgsqlServiceCollectionExtensions) - }; + ]; - public override HashSet UnmatchedMetadataMethods { get; } = new() - { + public override HashSet UnmatchedMetadataMethods { get; } = + [ typeof(NpgsqlPropertyBuilderExtensions).GetMethod( nameof(NpgsqlPropertyBuilderExtensions.IsGeneratedTsVectorColumn), - new[] { typeof(PropertyBuilder), typeof(string), typeof(string[]) }) - }; + [typeof(PropertyBuilder), typeof(string), typeof(string[])]) + ]; public override Dictionary MetadataMethodExceptions { get; } = new() - { + public override HashSet MetadataMethodExceptions { get; } = + [ typeof(NpgsqlEntityTypeExtensions).GetRuntimeMethod( nameof(NpgsqlEntityTypeExtensions.SetStorageParameter), - new[] { typeof(IConventionEntityType), typeof(string), typeof(object), typeof(bool) }) - }; + [typeof(IConventionEntityType), typeof(string), typeof(object), typeof(bool)]) + ]; } } diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs index 3f5aa7ae5..f5e354650 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs @@ -5,6 +5,7 @@ public class NpgsqlComplianceTest : RelationalComplianceTestBase protected override ICollection IgnoredTestBases { get; } = new HashSet { // Not implemented + typeof(CompiledModelTestBase), typeof(CompiledModelRelationalTestBase), // #3087 typeof(FromSqlSprocQueryTestBase<>), typeof(UdfDbFunctionTestBase<>), typeof(UpdateSqlGeneratorTestBase), diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs index 3e2e54569..9954a7b15 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs @@ -46,7 +46,7 @@ await context.Database.CreateExecutionStrategy().ExecuteAsync( [InlineData(false, true, true)] public async Task Returns_true_when_database_exists(bool async, bool ambientTransaction, bool useCanConnect) { - await using var testDatabase = NpgsqlTestStore.GetOrCreateInitialized("ExistingBlogging"); + await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); await using var context = new BloggingContext(testDatabase); var creator = GetDatabaseCreator(context); @@ -79,7 +79,7 @@ public class NpgsqlDatabaseCreatorEnsureDeletedTest : NpgsqlDatabaseCreatorTest [InlineData(false, true, false)] public async Task Deletes_database(bool async, bool open, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.CreateInitialized("EnsureDeleteBlogging"); + await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("EnsureDeleteBlogging"); if (!open) { testDatabase.CloseConnection(); @@ -166,11 +166,11 @@ private static async Task Creates_physical_database_and_schema_test( await using var context = new BloggingContext(testDatabase); if (createDatabase) { - testDatabase.Initialize(null, (Func)null); + await testDatabase.InitializeAsync(null, (Func)null); } else { - testDatabase.DeleteDatabase(); + await testDatabase.DeleteDatabaseAsync(); } var creator = GetDatabaseCreator(context); @@ -208,8 +208,7 @@ private static async Task Creates_physical_database_and_schema_test( Assert.Equal(14, columns.Length); Assert.Equal( - new[] - { + new[] { "Blogs.AndChew (bytea)", "Blogs.AndRow (bytea)", "Blogs.Cheese (text)", @@ -233,7 +232,7 @@ private static async Task Creates_physical_database_and_schema_test( [InlineData(false)] public async Task Noop_when_database_exists_and_has_schema(bool async) { - await using var testDatabase = NpgsqlTestStore.CreateInitialized("InitializedBlogging"); + await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("InitializedBlogging"); await using var context = new BloggingContext(testDatabase); context.Database.EnsureCreatedResiliently(); @@ -276,7 +275,7 @@ await databaseCreator.ExecutionStrategy.ExecuteAsync( [InlineData(false, true)] public async Task Returns_false_when_database_exists_but_has_no_tables(bool async, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.GetOrCreateInitialized("Empty"); + await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("Empty"); var creator = GetDatabaseCreator(testDatabase); await GetExecutionStrategy(testDatabase).ExecuteAsync( @@ -294,8 +293,8 @@ await GetExecutionStrategy(testDatabase).ExecuteAsync( [InlineData(false, false)] public async Task Returns_true_when_database_exists_and_has_any_tables(bool async, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.GetOrCreate("ExistingTables") - .InitializeNpgsql(null, t => new BloggingContext(t), null); + await using var testDatabase = await NpgsqlTestStore.GetOrCreate("ExistingTables") + .InitializeNpgsqlAsync(null, t => new BloggingContext(t), null); var creator = GetDatabaseCreator(testDatabase); await GetExecutionStrategy(testDatabase).ExecuteAsync( @@ -314,7 +313,7 @@ await GetExecutionStrategy(testDatabase).ExecuteAsync( [RequiresPostgis] public async Task Returns_false_when_database_exists_and_has_only_postgis_tables(bool async, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.GetOrCreateInitialized("Empty"); + await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("Empty"); testDatabase.ExecuteNonQuery("CREATE EXTENSION IF NOT EXISTS postgis"); var creator = GetDatabaseCreator(testDatabase); @@ -337,7 +336,7 @@ public class NpgsqlDatabaseCreatorDeleteTest : NpgsqlDatabaseCreatorTest [InlineData(false, false)] public static async Task Deletes_database(bool async, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.CreateInitialized("DeleteBlogging"); + await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("DeleteBlogging"); testDatabase.CloseConnection(); var creator = GetDatabaseCreator(testDatabase); @@ -385,7 +384,7 @@ public class NpgsqlDatabaseCreatorCreateTablesTest : NpgsqlDatabaseCreatorTest [InlineData(false, false)] public async Task Creates_schema_in_existing_database_test(bool async, bool ambientTransaction) { - await using var testDatabase = NpgsqlTestStore.GetOrCreateInitialized("ExistingBlogging" + (async ? "Async" : "")); + await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging" + (async ? "Async" : "")); await using var context = new BloggingContext(testDatabase); var creator = GetDatabaseCreator(context); @@ -453,37 +452,37 @@ public void GenerateCreateScript_works() using var context = new BloggingContext("Data Source=foo"); var script = context.Database.GenerateCreateScript(); Assert.Equal( - @"CREATE TABLE ""Blogs"" (" + """CREATE TABLE "Blogs" (""" + _eol - + @" ""Key1"" text NOT NULL," + + """ "Key1" text NOT NULL,""" + _eol - + @" ""Key2"" bytea NOT NULL," + + """ "Key2" bytea NOT NULL,""" + _eol - + @" ""Cheese"" text," + + """ "Cheese" text,""" + _eol - + @" ""ErMilan"" integer NOT NULL," + + """ "ErMilan" integer NOT NULL,""" + _eol - + @" ""George"" boolean NOT NULL," + + """ "George" boolean NOT NULL,""" + _eol - + @" ""TheGu"" uuid NOT NULL," + + """ "TheGu" uuid NOT NULL,""" + _eol - + @" ""NotFigTime"" timestamp with time zone NOT NULL," + + """ "NotFigTime" timestamp with time zone NOT NULL,""" + _eol - + @" ""ToEat"" smallint NOT NULL," + + """ "ToEat" smallint NOT NULL,""" + _eol - + @" ""OrNothing"" double precision NOT NULL," + + """ "OrNothing" double precision NOT NULL,""" + _eol - + @" ""Fuse"" smallint NOT NULL," + + """ "Fuse" smallint NOT NULL,""" + _eol - + @" ""WayRound"" bigint NOT NULL," + + """ "WayRound" bigint NOT NULL,""" + _eol - + @" ""On"" real NOT NULL," + + """ "On" real NOT NULL,""" + _eol - + @" ""AndChew"" bytea," + + """ "AndChew" bytea,""" + _eol - + @" ""AndRow"" bytea," + + """ "AndRow" bytea,""" + _eol - + @" CONSTRAINT ""PK_Blogs"" PRIMARY KEY (""Key1"", ""Key2"")" + + """ CONSTRAINT "PK_Blogs" PRIMARY KEY ("Key1", "Key2")""" + _eol + ");" + _eol @@ -540,7 +539,7 @@ await testDatabase.QueryAsync( [InlineData(false)] public async Task Throws_if_database_already_exists(bool async) { - await using var testDatabase = NpgsqlTestStore.GetOrCreateInitialized("ExistingBlogging"); + await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); var creator = GetDatabaseCreator(testDatabase); var ex = async @@ -571,13 +570,9 @@ protected static IExecutionStrategy GetExecutionStrategy(NpgsqlTestStore testSto => new BloggingContext(testStore).GetService().Create(); // ReSharper disable once ClassNeverInstantiated.Local - private class TestNpgsqlExecutionStrategyFactory : NpgsqlExecutionStrategyFactory + private class TestNpgsqlExecutionStrategyFactory(ExecutionStrategyDependencies dependencies) + : NpgsqlExecutionStrategyFactory(dependencies) { - public TestNpgsqlExecutionStrategyFactory(ExecutionStrategyDependencies dependencies) - : base(dependencies) - { - } - protected override IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) => new NonRetryingExecutionStrategy(dependencies); } @@ -589,24 +584,17 @@ private static IServiceProvider CreateServiceProvider() .AddScoped() .BuildServiceProvider(); - protected class BloggingContext : DbContext + protected class BloggingContext(string connectionString) : DbContext { - private readonly string _connectionString; - public BloggingContext(NpgsqlTestStore testStore) : this(testStore.ConnectionString) { } - public BloggingContext(string connectionString) - { - _connectionString = connectionString; - } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql( - _connectionString, b => b + connectionString, b => b .ApplyConfiguration() .SetPostgresVersion(TestEnvironment.PostgresVersion)) .UseInternalServiceProvider(CreateServiceProvider()); @@ -641,16 +629,13 @@ public class Blog public byte[] AndRow { get; set; } } - public class TestDatabaseCreator : NpgsqlDatabaseCreator + public class TestDatabaseCreator( + RelationalDatabaseCreatorDependencies dependencies, + INpgsqlRelationalConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalConnectionDiagnosticsLogger connectionLogger) + : NpgsqlDatabaseCreator(dependencies, connection, rawSqlCommandBuilder, connectionLogger) { - public TestDatabaseCreator( - RelationalDatabaseCreatorDependencies dependencies, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : base(dependencies, connection, rawSqlCommandBuilder) - { - } - public bool HasTablesBase() => HasTables(); diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs index efbc1e333..47173ca15 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs @@ -2,10 +2,4 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class NpgsqlServiceCollectionExtensionsTest : RelationalServiceCollectionExtensionsTestBase -{ - public NpgsqlServiceCollectionExtensionsTest() - : base(NpgsqlTestHelpers.Instance) - { - } -} +public class NpgsqlServiceCollectionExtensionsTest() : RelationalServiceCollectionExtensionsTestBase(NpgsqlTestHelpers.Instance); diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs index b7de014d9..bf6b1ca0d 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs @@ -8,9 +8,9 @@ public class NpgsqlValueGenerationScenariosTest private static readonly string DatabaseName = "NpgsqlValueGenerationScenariosTest"; [Fact] - public void Insert_with_sequence_id() + public async Task Insert_with_sequence_id() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextSequence(testStore.Name)) { @@ -30,18 +30,12 @@ public void Insert_with_sequence_id() } } - public class BlogContextSequence : ContextBase - { - public BlogContextSequence(string databaseName) - : base(databaseName) - { - } - } + public class BlogContextSequence(string databaseName) : ContextBase(databaseName); [Fact] - public void Insert_with_sequence_HiLo() + public async Task Insert_with_sequence_HiLo() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextHiLo(testStore.Name)) { @@ -61,13 +55,8 @@ public void Insert_with_sequence_HiLo() } } - public class BlogContextHiLo : ContextBase + public class BlogContextHiLo(string databaseName) : ContextBase(databaseName) { - public BlogContextHiLo(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -77,9 +66,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_default_value_from_sequence() + public async Task Insert_with_default_value_from_sequence() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextDefaultValue(testStore.Name)) { @@ -117,13 +106,8 @@ public void Insert_with_default_value_from_sequence() } } - public class BlogContextDefaultValue : ContextBase + public class BlogContextDefaultValue(string databaseName) : ContextBase(databaseName) { - public BlogContextDefaultValue(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -140,13 +124,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } - public class BlogContextDefaultValueNoMigrations : ContextBase + public class BlogContextDefaultValueNoMigrations(string databaseName) : ContextBase(databaseName) { - public BlogContextDefaultValueNoMigrations(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -165,9 +144,9 @@ public class BlogWithStringKey } [Fact] - public void Insert_with_key_default_value_from_sequence() + public async Task Insert_with_key_default_value_from_sequence() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) { @@ -187,13 +166,8 @@ public void Insert_with_key_default_value_from_sequence() } } - public class BlogContextKeyColumnWithDefaultValue : ContextBase + public class BlogContextKeyColumnWithDefaultValue(string databaseName) : ContextBase(databaseName) { - public BlogContextKeyColumnWithDefaultValue(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -211,9 +185,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [ConditionalFact] - public void Insert_uint_to_Identity_column_using_value_converter() + public async Task Insert_uint_to_Identity_column_using_value_converter() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) { context.Database.EnsureCreatedResiliently(); @@ -233,13 +207,8 @@ public void Insert_uint_to_Identity_column_using_value_converter() } } - public class BlogContextUIntToIdentityUsingValueConverter : ContextBase + public class BlogContextUIntToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) { - public BlogContextUIntToIdentityUsingValueConverter(string databaseName) - : base(databaseName) - { - } - public DbSet UnsignedBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -260,9 +229,9 @@ public class BlogWithUIntKey } [ConditionalFact] - public void Insert_string_to_Identity_column_using_value_converter() + public async Task Insert_string_to_Identity_column_using_value_converter() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) { context.Database.EnsureCreatedResiliently(); @@ -282,13 +251,8 @@ public void Insert_string_to_Identity_column_using_value_converter() } } - public class BlogContextStringToIdentityUsingValueConverter : ContextBase + public class BlogContextStringToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) { - public BlogContextStringToIdentityUsingValueConverter(string databaseName) - : base(databaseName) - { - } - public DbSet StringyBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -310,9 +274,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_explicit_non_default_keys() + public async Task Insert_with_explicit_non_default_keys() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextNoKeyGeneration(testStore.Name)) { @@ -332,13 +296,8 @@ public void Insert_with_explicit_non_default_keys() } } - public class BlogContextNoKeyGeneration : ContextBase + public class BlogContextNoKeyGeneration(string databaseName) : ContextBase(databaseName) { - public BlogContextNoKeyGeneration(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -351,9 +310,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_explicit_with_default_keys() + public async Task Insert_with_explicit_with_default_keys() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) { @@ -373,13 +332,8 @@ public void Insert_with_explicit_with_default_keys() } } - public class BlogContextNoKeyGenerationNullableKey : ContextBase + public class BlogContextNoKeyGenerationNullableKey(string databaseName) : ContextBase(databaseName) { - public BlogContextNoKeyGenerationNullableKey(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -392,9 +346,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_non_key_default_value() + public async Task Insert_with_non_key_default_value() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) { @@ -431,13 +385,8 @@ public void Insert_with_non_key_default_value() } } - public class BlogContextNonKeyDefaultValue : ContextBase + public class BlogContextNonKeyDefaultValue(string databaseName) : ContextBase(databaseName) { - public BlogContextNonKeyDefaultValue(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -450,9 +399,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_non_key_default_value_readonly() + public async Task Insert_with_non_key_default_value_readonly() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) { @@ -490,13 +439,8 @@ public void Insert_with_non_key_default_value_readonly() } } - public class BlogContextNonKeyReadOnlyDefaultValue : ContextBase + public class BlogContextNonKeyReadOnlyDefaultValue(string databaseName) : ContextBase(databaseName) { - public BlogContextNonKeyReadOnlyDefaultValue(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -509,9 +453,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_serial_non_id() + public async Task Insert_with_serial_non_id() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); int afterSave; @@ -533,13 +477,8 @@ public void Insert_with_serial_non_id() } } - public class BlogContextSequenceNonId : ContextBase + public class BlogContextSequenceNonId(string databaseName) : ContextBase(databaseName) { - public BlogContextSequenceNonId(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -552,9 +491,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } [Fact] - public void Insert_with_client_generated_GUID_key() + public async Task Insert_with_client_generated_GUID_key() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); Guid afterSave; using (var context = new BlogContext(testStore.Name)) @@ -574,18 +513,12 @@ public void Insert_with_client_generated_GUID_key() } } - public class BlogContext : ContextBase - { - public BlogContext(string databaseName) - : base(databaseName) - { - } - } + public class BlogContext(string databaseName) : ContextBase(databaseName); [Fact] - public void Insert_with_server_generated_GUID_key() + public async Task Insert_with_server_generated_GUID_key() { - using var testStore = NpgsqlTestStore.CreateInitialized(DatabaseName); + await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); Guid afterSave; using (var context = new BlogContextServerGuidKey(testStore.Name)) @@ -617,13 +550,8 @@ public void Insert_with_server_generated_GUID_key() } } - public class BlogContextServerGuidKey : ContextBase + public class BlogContextServerGuidKey(string databaseName) : ContextBase(databaseName) { - public BlogContextServerGuidKey(string databaseName) - : base(databaseName) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasPostgresExtension("uuid-ossp"); @@ -676,15 +604,8 @@ public class ConcurrentBlog public byte[] Timestamp { get; set; } } - public abstract class ContextBase : DbContext + public abstract class ContextBase(string databaseName) : DbContext { - private readonly string _databaseName; - - protected ContextBase(string databaseName) - { - _databaseName = databaseName; - } - public DbSet Blogs { get; set; } public DbSet NullableKeyBlogs { get; set; } public DbSet FullNameBlogs { get; set; } @@ -695,7 +616,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .EnableServiceProviderCaching(false) .UseNpgsql( - NpgsqlTestStore.CreateConnectionString(_databaseName), + NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs index fbccf7ceb..e68095f3c 100644 --- a/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs @@ -2,32 +2,16 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class OptimisticConcurrencyBytesNpgsqlTest : OptimisticConcurrencyNpgsqlTestBase -{ - public OptimisticConcurrencyBytesNpgsqlTest(F1BytesNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class OptimisticConcurrencyBytesNpgsqlTest(F1BytesNpgsqlFixture fixture) + : OptimisticConcurrencyNpgsqlTestBase(fixture); // uint maps directly to xid, which is the PG type of the xmin column that we use as a row version. -public class OptimisticConcurrencyNpgsqlTest : OptimisticConcurrencyNpgsqlTestBase -{ - public OptimisticConcurrencyNpgsqlTest(F1NpgsqlFixture fixture) - : base(fixture) - { - } -} +public class OptimisticConcurrencyNpgsqlTest(F1NpgsqlFixture fixture) : OptimisticConcurrencyNpgsqlTestBase(fixture); -public abstract class OptimisticConcurrencyNpgsqlTestBase - : OptimisticConcurrencyRelationalTestBase +public abstract class OptimisticConcurrencyNpgsqlTestBase(TFixture fixture) + : OptimisticConcurrencyRelationalTestBase(fixture) where TFixture : F1RelationalFixture, new() { - protected OptimisticConcurrencyNpgsqlTestBase(TFixture fixture) - : base(fixture) - { - } - [ConditionalFact] public async Task Modifying_concurrency_token_only_is_noop() { diff --git a/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs index 51650b7bd..103eea732 100644 --- a/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class OverzealousInitializationNpgsqlTest - : OverzealousInitializationTestBase +public class OverzealousInitializationNpgsqlTest(OverzealousInitializationNpgsqlTest.OverzealousInitializationNpgsqlFixture fixture) + : OverzealousInitializationTestBase(fixture) { - public OverzealousInitializationNpgsqlTest(OverzealousInitializationNpgsqlFixture fixture) - : base(fixture) - { - } - public class OverzealousInitializationNpgsqlFixture : OverzealousInitializationFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs index 7473cba9d..2f9d6bfed 100644 --- a/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class PropertyValuesNpgsqlTest : PropertyValuesTestBase +public class PropertyValuesNpgsqlTest(PropertyValuesNpgsqlTest.PropertyValuesNpgsqlFixture fixture) + : PropertyValuesTestBase(fixture) { - public PropertyValuesNpgsqlTest(PropertyValuesNpgsqlFixture fixture) - : base(fixture) - { - } - public class PropertyValuesNpgsqlFixture : PropertyValuesFixtureBase { protected override string StoreName { get; } = "PropertyValues"; diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs new file mode 100644 index 000000000..a3bafed45 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs @@ -0,0 +1,39 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocAdvancedMappingsQueryNpgsqlTest : AdHocAdvancedMappingsQueryRelationalTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + // Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported. + public override Task Query_generates_correct_datetime2_parameter_definition(int? fractionalSeconds, string postfix) + => Assert.ThrowsAsync( + () => base.Query_generates_correct_datetime2_parameter_definition(fractionalSeconds, postfix)); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Query_generates_correct_datetimeoffset_parameter_definition(int? fractionalSeconds, string postfix) + => Assert.ThrowsAsync( + () => base.Query_generates_correct_datetime2_parameter_definition(fractionalSeconds, postfix)); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Projecting_one_of_two_similar_complex_types_picks_the_correct_one() + => Assert.ThrowsAsync( + () => base.Projecting_one_of_two_similar_complex_types_picks_the_correct_one()); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Projecting_expression_with_converter_with_closure(bool async) + => Assert.ThrowsAsync( + () => base.Projecting_one_of_two_similar_complex_types_picks_the_correct_one()); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Projecting_property_with_converter_with_closure(bool async) + => Assert.ThrowsAsync( + () => base.Projecting_one_of_two_similar_complex_types_picks_the_correct_one()); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Projecting_property_with_converter_without_closure(bool async) + => Assert.ThrowsAsync( + () => base.Projecting_one_of_two_similar_complex_types_picks_the_correct_one()); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocComplexTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocComplexTypeQueryNpgsqlTest.cs new file mode 100644 index 000000000..cf86a2b2f --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocComplexTypeQueryNpgsqlTest.cs @@ -0,0 +1,32 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocComplexTypeQueryNpgsqlTest : AdHocComplexTypeQueryTestBase +{ + public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() + { + await base.Complex_type_equals_parameter_with_nested_types_with_property_of_same_name(); + + AssertSql( + """ +@__entity_equality_container_0_Id='1' (Nullable = true) +@__entity_equality_container_0_Containee1_Id='2' (Nullable = true) +@__entity_equality_container_0_Containee2_Id='3' (Nullable = true) + +SELECT e."Id", e."ComplexContainer_Id", e."ComplexContainer_Containee1_Id", e."ComplexContainer_Containee2_Id" +FROM "EntityType" AS e +WHERE e."ComplexContainer_Id" = @__entity_equality_container_0_Id AND e."ComplexContainer_Containee1_Id" = @__entity_equality_container_0_Containee1_Id AND e."ComplexContainer_Containee2_Id" = @__entity_equality_container_0_Containee2_Id +LIMIT 2 +"""); + } + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocJsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocJsonQueryNpgsqlTest.cs new file mode 100644 index 000000000..056aab9d1 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocJsonQueryNpgsqlTest.cs @@ -0,0 +1,271 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocJsonQueryNpgsqlTest : AdHocJsonQueryTestBase +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override async Task Seed29219(DbContext ctx) + { + var entity1 = new MyEntity29219 + { + Id = 1, + Reference = new MyJsonEntity29219 { NonNullableScalar = 10, NullableScalar = 11 }, + Collection = + [ + new() { NonNullableScalar = 100, NullableScalar = 101 }, + new() { NonNullableScalar = 200, NullableScalar = 201 }, + new() { NonNullableScalar = 300, NullableScalar = null } + ] + }; + + var entity2 = new MyEntity29219 + { + Id = 2, + Reference = new MyJsonEntity29219 { NonNullableScalar = 20, NullableScalar = null }, + Collection = [new() { NonNullableScalar = 1001, NullableScalar = null }] + }; + + ctx.AddRange(entity1, entity2); + await ctx.SaveChangesAsync(); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Id", "Reference", "Collection") +VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') +"""); + } + + protected override async Task Seed30028(DbContext ctx) + { + // complete + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +1, +'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') +"""); + + // missing collection + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +2, +'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') +"""); + + // missing optional reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +3, +'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') +"""); + + // missing required reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +4, +'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') +"""); + } + + protected override async Task Seed33046(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Reviews" ("Rounds", "Id") +VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) +"""); + + protected override async Task SeedArrayOfPrimitives(DbContext ctx) + { + var entity1 = new MyEntityArrayOfPrimitives + { + Id = 1, + Reference = new MyJsonEntityArrayOfPrimitives + { + IntArray = [1, 2, 3], + ListOfString = + [ + "Foo", + "Bar", + "Baz" + ] + }, + Collection = + [ + new() { IntArray = [111, 112, 113], ListOfString = ["Foo11", "Bar11"] }, + new() { IntArray = [211, 212, 213], ListOfString = ["Foo12", "Bar12"] } + ] + }; + + var entity2 = new MyEntityArrayOfPrimitives + { + Id = 2, + Reference = new MyJsonEntityArrayOfPrimitives + { + IntArray = [10, 20, 30], + ListOfString = + [ + "A", + "B", + "C" + ] + }, + Collection = + [ + new() { IntArray = [110, 120, 130], ListOfString = ["A1", "Z1"] }, + new() { IntArray = [210, 220, 230], ListOfString = ["A2", "Z2"] } + ] + }; + + ctx.AddRange(entity1, entity2); + await ctx.SaveChangesAsync(); + } + + protected override async Task SeedJunkInJson(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id") +VALUES( +'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', +'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', +'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', +'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', +1) +"""); + + protected override async Task SeedTrickyBuffering(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Reference", "Id") +VALUES( +'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00Z"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00Z", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00Z"}]}',1) +"""); + + protected override async Task SeedShadowProperties(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id", "Name") +VALUES( +'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', +'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', +'{"Name":"e1_r", "ShadowString":"Foo"}', +'{"ShadowInt":143,"Name":"e1_r ctor"}', +1, +'e1') +"""); + + protected override async Task SeedNotICollection(DbContext ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Json", "Id") +VALUES( +'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', +1) +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Json", "Id") +VALUES( +'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', +2) +"""); + } + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Json_predicate_on_bytea(bool async) + { + var contextFactory = await InitializeAsync( + seed: async context => + { + context.Entities.AddRange( + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = [1, 2, 3] } }, + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = [1, 2, 4] } }); + await context.SaveChangesAsync(); + }); + + using (var context = contextFactory.CreateContext()) + { + var query = context.Entities.Where(x => x.JsonEntity.Bytea == new byte[] { 1, 2, 4 }); + + var result = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal(2, result.Id); + + AssertSql( + """ +SELECT e."Id", e."JsonEntity" +FROM "Entities" AS e +WHERE (decode(e."JsonEntity" ->> 'Bytea', 'base64')) = BYTEA E'\\x010204' +LIMIT 2 +"""); + } + } + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Json_predicate_on_interval(bool async) + { + var contextFactory = await InitializeAsync( + seed: async context => + { + context.Entities.AddRange( + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(1, 2, 3, 4, 123, 456) } }, + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(2, 2, 3, 4, 123, 456) } }); + await context.SaveChangesAsync(); + }); + + using (var context = contextFactory.CreateContext()) + { + var query = context.Entities.Where(x => x.JsonEntity.Interval == new TimeSpan(2, 2, 3, 4, 123, 456)); + + var result = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal(2, result.Id); + + AssertSql( + """ +SELECT e."Id", e."JsonEntity" +FROM "Entities" AS e +WHERE (CAST(e."JsonEntity" ->> 'Interval' AS interval)) = INTERVAL '2 02:03:04.123456' +LIMIT 2 +"""); + } + } + + protected class TypesDbContext(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().OwnsOne(b => b.JsonEntity).ToJson(); + } + + public class TypesContainerEntity + { + public int Id { get; set; } + public TypesJsonEntity JsonEntity { get; set; } + } + + public class TypesJsonEntity + { + public byte[] Bytea { get; set; } + public TimeSpan Interval { get; set; } + } + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ManyToManyHeterogeneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs similarity index 68% rename from test/EFCore.PG.FunctionalTests/Query/ManyToManyHeterogeneousQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs index 6f13329a9..eb60e4c60 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ManyToManyHeterogeneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs @@ -2,7 +2,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class ManyToManyHeterogeneousQuerySqlServerTest : ManyToManyHeterogeneousQueryRelationalTestBase +public class AdHocManyToManyQueryNpgsqlTest : AdHocManyToManyQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/Query/SimpleQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs similarity index 71% rename from test/EFCore.PG.FunctionalTests/Query/SimpleQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs index 1861efae3..cf497d6a5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SimpleQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs @@ -2,11 +2,18 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class SimpleQueryNpgsqlTest : SimpleQueryRelationalTestBase +public class AdHocMiscellaneousQueryNpgsqlTest : AdHocMiscellaneousQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + protected override Task Seed2951(Context2951 context) + => context.Database.ExecuteSqlRawAsync( + """ +CREATE TABLE "ZeroKey" ("Id" int); +INSERT INTO "ZeroKey" VALUES (NULL) +"""); + // Writes DateTime with Kind=Unspecified to timestamptz public override Task SelectMany_where_Select(bool async) => Task.CompletedTask; diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs new file mode 100644 index 000000000..90feaf094 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs @@ -0,0 +1,13 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocNavigationsQueryNpgsqlTest : AdHocNavigationsQueryRelationalTestBase +{ + // Cannot write DateTime with Kind=Local to PostgreSQL type 'timestamp with time zone', only UTC is supported. + public override Task Reference_include_on_derived_type_with_sibling_works() + => Assert.ThrowsAsync(() => base.Reference_include_on_derived_type_with_sibling_works()); + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs new file mode 100644 index 000000000..8839ca80e --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs @@ -0,0 +1,120 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocPrecompiledQueryNpgsqlTest(ITestOutputHelper testOutputHelper) + : AdHocPrecompiledQueryRelationalTestBase(testOutputHelper) +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public override async Task Index_no_evaluatability() + { + await base.Index_no_evaluatability(); + + AssertSql( + """ +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE j."IntList"[j."Id" + 1] = 2 +"""); + } + + public override async Task Index_with_captured_variable() + { + await base.Index_with_captured_variable(); + + AssertSql( + """ +@__id_0='1' + +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE j."IntList"[@__id_0 + 1] = 2 +"""); + } + + public override async Task JsonScalar() + { + await base.JsonScalar(); + + AssertSql( + """ +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE (j."JsonThing" ->> 'StringProperty') = 'foo' +"""); + } + + public override async Task Materialize_non_public() + { + await base.Materialize_non_public(); + + AssertSql( + """ +@p0='10' (Nullable = true) +@p1='9' (Nullable = true) +@p2='8' (Nullable = true) + +INSERT INTO "NonPublicEntities" ("PrivateAutoProperty", "PrivateProperty", "_privateField") +VALUES (@p0, @p1, @p2) +RETURNING "Id"; +""", + // + """ +SELECT n."Id", n."PrivateAutoProperty", n."PrivateProperty", n."_privateField" +FROM "NonPublicEntities" AS n +LIMIT 2 +"""); + } + + public override async Task Projecting_property_requiring_converter_with_closure_is_not_supported() + { + await base.Projecting_property_requiring_converter_with_closure_is_not_supported(); + + AssertSql(); + } + + public override async Task Projecting_expression_requiring_converter_without_closure_works() + { + await base.Projecting_expression_requiring_converter_without_closure_works(); + + AssertSql( + """ +SELECT b."AudiobookDate" +FROM "Books" AS b +"""); + } + + public override async Task Projecting_entity_with_property_requiring_converter_with_closure_works() + { + await base.Projecting_entity_with_property_requiring_converter_with_closure_works(); + + AssertSql( + """ +SELECT b."Id", b."AudiobookDate", b."Name", b."PublishDate" +FROM "Books" AS b +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers + => NpgsqlPrecompiledQueryTestHelpers.Instance; + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var sqlServerOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + sqlServerOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocQueryFiltersQueryNpgsqlTest.cs similarity index 52% rename from test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlFixture.cs rename to test/EFCore.PG.FunctionalTests/Query/AdHocQueryFiltersQueryNpgsqlTest.cs index 497d71540..a6c0b0c17 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocQueryFiltersQueryNpgsqlTest.cs @@ -1,8 +1,8 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class NullSemanticsQueryNpgsqlFixture : NullSemanticsQueryFixtureBase +public class AdHocQueryFiltersQueryNpgsqlTest : AdHocQueryFiltersQueryRelationalTestBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs new file mode 100644 index 000000000..927fe2ff8 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs @@ -0,0 +1,40 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class AdHocQuerySplittingQueryNpgsqlTest : AdHocQuerySplittingQueryTestBase +{ + protected override DbContextOptionsBuilder SetQuerySplittingBehavior( + DbContextOptionsBuilder optionsBuilder, + QuerySplittingBehavior splittingBehavior) + { + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseQuerySplittingBehavior(splittingBehavior); + + return optionsBuilder; + } + + protected override DbContextOptionsBuilder ClearQuerySplittingBehavior(DbContextOptionsBuilder optionsBuilder) + { + var extension = optionsBuilder.Options.FindExtension(); + if (extension == null) + { + extension = new NpgsqlOptionsExtension(); + } + else + { + _querySplittingBehaviorFieldInfo.SetValue(extension, null); + } + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + + private static readonly FieldInfo _querySplittingBehaviorFieldInfo = + typeof(RelationalOptionsExtension).GetField("_querySplittingBehavior", BindingFlags.NonPublic | BindingFlags.Instance); + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs index 5703c1eeb..b89306d03 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class ArrayArrayQueryTest : ArrayQueryTest +public class ArrayArrayQueryTest(ArrayArrayQueryTest.ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) + : ArrayQueryTest(fixture, testOutputHelper) { - public ArrayArrayQueryTest(ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - #region Indexers public override async Task Index_with_constant(bool async) @@ -17,7 +13,7 @@ public override async Task Index_with_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray"[1] = 3 """); @@ -31,7 +27,7 @@ public override async Task Index_with_parameter(bool async) """ @__x_0='0' -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray"[@__x_0 + 1] = 3 """); @@ -43,7 +39,7 @@ public override async Task Nullable_index_with_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntArray"[1] = 3 """); @@ -55,7 +51,7 @@ public override async Task Nullable_value_array_index_compare_to_null(bool async AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntArray"[3] IS NULL """); @@ -67,7 +63,7 @@ public override async Task Non_nullable_value_array_index_compare_to_null(bool a AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE FALSE """); @@ -79,7 +75,7 @@ public override async Task Nullable_reference_array_index_compare_to_null(bool a AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableStringArray"[3] IS NULL """); @@ -91,7 +87,7 @@ public override async Task Non_nullable_reference_array_index_compare_to_null(bo AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE FALSE """); @@ -107,7 +103,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE get_byte(s."Bytea", 0) = 3 """); @@ -125,7 +121,7 @@ public override async Task SequenceEqual_with_parameter(bool async) """ @__arr_0={ '3', '4' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" = @__arr_0 """); @@ -151,7 +147,7 @@ public override async Task SequenceEqual_over_nullable_with_parameter(bool async """ @__arr_0={ '3', '4', NULL } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntArray" = @__arr_0 """); @@ -167,7 +163,7 @@ public override async Task Array_column_Any_equality_operator(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."StringArray" @> ARRAY['3']::text[] """); @@ -179,7 +175,7 @@ public override async Task Array_column_Any_Equals(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."StringArray" @> ARRAY['3']::text[] """); @@ -191,7 +187,7 @@ public override async Task Array_column_Contains_literal_item(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" @> ARRAY[3]::integer[] """); @@ -205,7 +201,7 @@ public override async Task Array_column_Contains_parameter_item(bool async) """ @__p_0='3' -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" @> ARRAY[@__p_0]::integer[] """); @@ -217,7 +213,7 @@ public override async Task Array_column_Contains_column_item(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" @> ARRAY[s."Id" + 2]::integer[] """); @@ -229,7 +225,7 @@ public override async Task Array_column_Contains_null_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE array_position(s."NullableStringArray", NULL) IS NOT NULL """); @@ -261,7 +257,7 @@ public override async Task Nullable_array_column_Contains_literal_item(bool asyn AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntArray" @> ARRAY[3]::integer[] """); @@ -275,7 +271,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" IN ('foo', 'xxx') """); @@ -293,7 +289,7 @@ await AssertQuery( """ @__array_0={ 'foo', 'xxx' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" = ANY (@__array_0) OR (s."NullableText" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -311,7 +307,7 @@ await AssertQuery( """ @__array_0={ '1' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Id" = ANY (@__array_0) """); @@ -401,7 +397,7 @@ await AssertQuery( """ @__values_0={ '1', '999' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Id"::text = ANY (@__values_0) """); @@ -420,7 +416,7 @@ await AssertQuery( """ @__values_0='0x14' (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Byte" = ANY (@__values_0) """); @@ -438,7 +434,7 @@ await AssertQuery( """ @__array_0={ '-2', '-3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."EnumConvertedToInt" = ANY (@__array_0) """); @@ -456,7 +452,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."EnumConvertedToString" = ANY (@__array_0) """); @@ -474,7 +470,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableEnumConvertedToString" = ANY (@__array_0) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -492,7 +488,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableEnumConvertedToStringWithNonNullableLambda" = ANY (@__array_0) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -504,15 +500,15 @@ public override async Task Array_column_Contains_value_converted_param(bool asyn await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedArray.Contains(item))); + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(item))); AssertSql( """ @__item_0='Eight' (Nullable = false) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedArray" @> ARRAY[@__item_0]::text[] +WHERE s."ValueConvertedArrayOfEnum" @> ARRAY[@__item_0]::text[] """); } @@ -520,13 +516,13 @@ public override async Task Array_column_Contains_value_converted_constant(bool a { await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedArray.Contains(SomeEnum.Eight))); + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(SomeEnum.Eight))); AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedArray" @> ARRAY['Eight']::text[] +WHERE s."ValueConvertedArrayOfEnum" @> ARRAY['Eight']::text[] """); } @@ -536,15 +532,15 @@ public override async Task Array_param_Contains_value_converted_array_column(boo await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedArray.All(x => p.Contains(x)))); + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.All(x => p.Contains(x)))); AssertSql( """ @__p_0={ 'Eight', 'Nine' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedArray" <@ @__p_0 +WHERE s."ValueConvertedArrayOfEnum" <@ @__p_0 """); } @@ -571,7 +567,7 @@ public override async Task IList_column_contains_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IList" @> ARRAY[10]::integer[] """); @@ -589,7 +585,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntArray") = 2 """); @@ -603,7 +599,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."NullableIntArray") = 3 """); @@ -617,7 +613,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntArray") = 2 """); @@ -633,7 +629,7 @@ public override async Task Any_no_predicate(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntArray") > 0 """); @@ -650,7 +646,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) """); @@ -667,7 +663,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) """); @@ -689,11 +685,11 @@ await AssertQuery( AssertSql( """ -@__patternsActual_0={ 'a%', 'b%', 'c%' } (DbType = Object) +@__patternsActual_1={ 'a%', 'b%', 'c%' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."NullableText" LIKE ANY (@__patternsActual_0) +WHERE s."NullableText" LIKE ANY (@__patternsActual_1) """); } @@ -708,7 +704,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) """); @@ -725,7 +721,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) """); @@ -737,7 +733,7 @@ public override async Task Any_Contains_on_constant_array(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE ARRAY[2,3]::integer[] && s."IntArray" """); @@ -751,7 +747,7 @@ public override async Task Any_Contains_between_column_and_List(bool async) """ @__ints_0={ '2', '3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" && @__ints_0 """); @@ -765,7 +761,7 @@ public override async Task Any_Contains_between_column_and_array(bool async) """ @__ints_0={ '2', '3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntArray" && @__ints_0 """); @@ -777,15 +773,15 @@ public override async Task Any_Contains_between_column_and_other_type(bool async await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedArray.Any(i => list.Contains(i)))); + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Any(i => list.Contains(i)))); AssertSql( """ @__list_0={ 'Eight' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedArray" && @__list_0 +WHERE s."ValueConvertedArrayOfEnum" && @__list_0 """); } @@ -795,7 +791,7 @@ public override async Task All_Contains(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE ARRAY[5,6]::integer[] <@ s."IntArray" """); @@ -837,7 +833,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE COALESCE(array_position(s."IntArray", 6) - 1, -1) = 1 """); @@ -851,25 +847,54 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE COALESCE(array_position(s."IntArray", 6, 2) - 1, -1) = 1 """); } // Note: see NorthwindFunctionsQueryNpgsqlTest.String_Join_non_aggregate for regular use without an array column/parameter - public override async Task String_Join_with_array_parameter(bool async) + public override async Task String_Join_with_array_of_int_column(bool async) { - await base.String_Join_with_array_parameter(async); + await base.String_Join_with_array_of_int_column(async); AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE array_to_string(s."IntArray", ', ', '') = '3, 4' """); } + public override async Task String_Join_with_array_of_string_column(bool async) + { + // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus + // ArrayToListReplacingExpressionVisitor won't work. + await AssertQuery( + async, + ss => ss.Set() + .Where(e => string.Join(", ", e.StringArray) == "3, 4")); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" +FROM "SomeEntities" AS s +WHERE array_to_string(s."StringArray", ', ', '') = '3, 4' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task String_Join_disallow_non_array_type_mapped_parameter(bool async) + { + // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus + // ArrayToListReplacingExpressionVisitor won't work. + await AssertTranslationFailed(() => AssertQuery( + async, + ss => ss.Set() + .Where(e => string.Join(", ", e.ArrayOfStringConvertedToDelimitedString) == "3, 4"))); + } + #endregion Other translations public class ArrayArrayQueryFixture : ArrayQueryFixture diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs index 98a30442c..99e88ded6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs @@ -18,7 +18,7 @@ public override async Task Index_with_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList"[1] = 3 """); @@ -32,7 +32,7 @@ public override async Task Index_with_parameter(bool async) """ @__x_0='0' -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList"[@__x_0 + 1] = 3 """); @@ -44,7 +44,7 @@ public override async Task Nullable_index_with_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntList"[1] = 3 """); @@ -56,7 +56,7 @@ public override async Task Nullable_value_array_index_compare_to_null(bool async AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntList"[3] IS NULL """); @@ -68,7 +68,7 @@ public override async Task Non_nullable_value_array_index_compare_to_null(bool a AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE FALSE """); @@ -80,7 +80,7 @@ public override async Task Nullable_reference_array_index_compare_to_null(bool a AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableStringList"[3] IS NULL """); @@ -92,7 +92,7 @@ public override async Task Non_nullable_reference_array_index_compare_to_null(bo AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE FALSE """); @@ -110,7 +110,7 @@ public override async Task SequenceEqual_with_parameter(bool async) """ @__arr_0={ '3', '4' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" = @__arr_0 """); @@ -136,7 +136,7 @@ public override async Task SequenceEqual_over_nullable_with_parameter(bool async """ @__arr_0={ '3', '4', NULL } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntList" = @__arr_0 """); @@ -152,7 +152,7 @@ public override async Task Array_column_Any_equality_operator(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."StringList" @> ARRAY['3']::text[] """); @@ -164,7 +164,7 @@ public override async Task Array_column_Any_Equals(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."StringList" @> ARRAY['3']::text[] """); @@ -176,7 +176,7 @@ public override async Task Array_column_Contains_literal_item(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" @> ARRAY[3]::integer[] """); @@ -190,7 +190,7 @@ public override async Task Array_column_Contains_parameter_item(bool async) """ @__p_0='3' -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" @> ARRAY[@__p_0]::integer[] """); @@ -202,7 +202,7 @@ public override async Task Array_column_Contains_column_item(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" @> ARRAY[s."Id" + 2]::integer[] """); @@ -214,7 +214,7 @@ public override async Task Array_column_Contains_null_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE array_position(s."NullableStringList", NULL) IS NOT NULL """); @@ -246,7 +246,7 @@ public override async Task Nullable_array_column_Contains_literal_item(bool asyn AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableIntList" @> ARRAY[3]::integer[] """); @@ -260,7 +260,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" IN ('foo', 'xxx') """); @@ -278,7 +278,7 @@ await AssertQuery( """ @__array_0={ 'foo', 'xxx' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" = ANY (@__array_0) OR (s."NullableText" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -296,7 +296,7 @@ await AssertQuery( """ @__array_0={ '1' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Id" = ANY (@__array_0) """); @@ -406,7 +406,7 @@ await AssertQuery( """ @__values_0={ '1', '999' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Id"::text = ANY (@__values_0) """); @@ -424,7 +424,7 @@ await AssertQuery( """ @__values_0={ '20' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."Byte" = ANY (@__values_0) """); @@ -442,7 +442,7 @@ await AssertQuery( """ @__array_0={ '-2', '-3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."EnumConvertedToInt" = ANY (@__array_0) """); @@ -460,7 +460,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."EnumConvertedToString" = ANY (@__array_0) """); @@ -478,7 +478,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableEnumConvertedToString" = ANY (@__array_0) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -496,7 +496,7 @@ await AssertQuery( """ @__array_0={ 'Two', 'Three' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableEnumConvertedToStringWithNonNullableLambda" = ANY (@__array_0) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@__array_0, NULL) IS NOT NULL) """); @@ -508,15 +508,15 @@ public override async Task Array_column_Contains_value_converted_param(bool asyn await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedList.Contains(item))); + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(item))); AssertSql( """ @__item_0='Eight' (Nullable = false) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedList" @> ARRAY[@__item_0]::text[] +WHERE s."ValueConvertedListOfEnum" @> ARRAY[@__item_0]::text[] """); } @@ -524,13 +524,13 @@ public override async Task Array_column_Contains_value_converted_constant(bool a { await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedList.Contains(SomeEnum.Eight))); + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(SomeEnum.Eight))); AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedList" @> ARRAY['Eight']::text[] +WHERE s."ValueConvertedListOfEnum" @> ARRAY['Eight']::text[] """); } @@ -540,15 +540,15 @@ public override async Task Array_param_Contains_value_converted_array_column(boo await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedArray.All(x => p.Contains(x)))); + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.All(x => p.Contains(x)))); AssertSql( """ @__p_0={ 'Eight', 'Nine' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedList" <@ @__p_0 +WHERE s."ValueConvertedListOfEnum" <@ @__p_0 """); } @@ -558,7 +558,7 @@ public override async Task IList_column_contains_constant(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IList" @> ARRAY[10]::integer[] """); @@ -593,7 +593,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntList") = 2 """); @@ -607,7 +607,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."NullableIntList") = 3 """); @@ -621,7 +621,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntList") = 2 """); @@ -637,7 +637,7 @@ public override async Task Any_no_predicate(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE cardinality(s."IntList") > 0 """); @@ -654,7 +654,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) """); @@ -671,7 +671,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) """); @@ -703,11 +703,11 @@ await AssertQuery( AssertSql( """ -@__patternsActual_0={ 'a%', 'b%', 'c%' } (DbType = Object) +@__patternsActual_1={ 'a%', 'b%', 'c%' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."NullableText" LIKE ANY (@__patternsActual_0) +WHERE s."NullableText" LIKE ANY (@__patternsActual_1) """); } @@ -722,7 +722,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) """); @@ -739,7 +739,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) """); @@ -751,7 +751,7 @@ public override async Task Any_Contains_on_constant_array(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE ARRAY[2,3]::integer[] && s."IntList" """); @@ -765,7 +765,7 @@ public override async Task Any_Contains_between_column_and_List(bool async) """ @__ints_0={ '2', '3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" && @__ints_0 """); @@ -779,7 +779,7 @@ public override async Task Any_Contains_between_column_and_array(bool async) """ @__ints_0={ '2', '3' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE s."IntList" && @__ints_0 """); @@ -791,15 +791,15 @@ public override async Task Any_Contains_between_column_and_other_type(bool async await AssertQuery( async, - ss => ss.Set().Where(e => e.ValueConvertedList.Any(i => array.Contains(i)))); + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Any(i => array.Contains(i)))); AssertSql( """ @__array_0={ 'Eight' } (DbType = Object) -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s -WHERE s."ValueConvertedList" && @__array_0 +WHERE s."ValueConvertedListOfEnum" && @__array_0 """); } @@ -809,7 +809,7 @@ public override async Task All_Contains(bool async) AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE ARRAY[5,6]::integer[] <@ s."IntList" """); @@ -853,7 +853,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE COALESCE(array_position(s."IntList", 6) - 1, -1) = 1 """); @@ -869,25 +869,54 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE COALESCE(array_position(s."IntList", 6, 2) - 1, -1) = 1 """); } // Note: see NorthwindFunctionsQueryNpgsqlTest.String_Join_non_aggregate for regular use without an array column/parameter - public override async Task String_Join_with_array_parameter(bool async) + public override async Task String_Join_with_array_of_int_column(bool async) { - await base.String_Join_with_array_parameter(async); + await base.String_Join_with_array_of_int_column(async); AssertSql( """ -SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArray", s."ValueConvertedList", s."Varchar10", s."Varchar15" +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" FROM "SomeEntities" AS s WHERE array_to_string(s."IntList", ', ', '') = '3, 4' """); } + public override async Task String_Join_with_array_of_string_column(bool async) + { + // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus + // ArrayToListReplacingExpressionVisitor won't work. + await AssertQuery( + async, + ss => ss.Set() + .Where(e => string.Join(", ", e.StringList) == "3, 4")); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15" +FROM "SomeEntities" AS s +WHERE array_to_string(s."StringList", ', ', '') = '3, 4' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task String_Join_disallow_non_array_type_mapped_parameter(bool async) + { + // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus + // ArrayToListReplacingExpressionVisitor won't work. + await AssertTranslationFailed(() => AssertQuery( + async, + ss => ss.Set() + .Where(e => string.Join(", ", e.ListOfStringConvertedToDelimitedString) == "3, 4"))); + } + #endregion Other translations public class ArrayListQueryFixture : ArrayQueryFixture @@ -925,11 +954,11 @@ private static readonly PropertyInfo StringList private static readonly PropertyInfo NullableStringList = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableStringList)); - private static readonly PropertyInfo ValueConvertedArray - = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedArray)); + private static readonly PropertyInfo ValueConvertedArrayOfEnum + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedArrayOfEnum)); - private static readonly PropertyInfo ValueConvertedList - = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedList)); + private static readonly PropertyInfo ValueConvertedListOfEnum + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedListOfEnum)); protected override Expression VisitMember(MemberExpression node) { @@ -953,9 +982,9 @@ protected override Expression VisitMember(MemberExpression node) return Expression.MakeMemberAccess(node.Expression, NullableStringList); } - if (node.Member == ValueConvertedArray) + if (node.Member == ValueConvertedArrayOfEnum) { - return Expression.MakeMemberAccess(node.Expression, ValueConvertedList); + return Expression.MakeMemberAccess(node.Expression, ValueConvertedListOfEnum); } return node; @@ -968,7 +997,7 @@ protected override Expression VisitBinary(BinaryExpression node) var listExpression = Visit(node.Left); if (listExpression.Type.IsGenericList()) { - var getItemMethod = listExpression.Type.GetMethod("get_Item", new[] { typeof(int) })!; + var getItemMethod = listExpression.Type.GetMethod("get_Item", [typeof(int)])!; return Expression.Call(listExpression, getItemMethod, node.Right); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs index e1b529203..1809ce560 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs @@ -3,7 +3,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public abstract class ArrayQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase +public abstract class ArrayQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; @@ -16,8 +16,8 @@ public TestSqlLoggerFactory TestSqlLoggerFactory public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer)); - protected override void Seed(ArrayQueryContext context) - => ArrayQueryContext.Seed(context); + protected override Task SeedAsync(ArrayQueryContext context) + => ArrayQueryContext.SeedAsync(context); public Func GetContextCreator() => CreateContext; @@ -56,8 +56,10 @@ public IReadOnlyDictionary EntityAsserters Assert.Equal(ee.NullableText, ee.NullableText); Assert.Equal(ee.NonNullableText, ee.NonNullableText); Assert.Equal(ee.EnumConvertedToInt, ee.EnumConvertedToInt); - Assert.Equal(ee.ValueConvertedArray, ee.ValueConvertedArray); - Assert.Equal(ee.ValueConvertedList, ee.ValueConvertedList); + Assert.Equal(ee.ArrayOfStringConvertedToDelimitedString, ee.ArrayOfStringConvertedToDelimitedString); + Assert.Equal(ee.ListOfStringConvertedToDelimitedString, ee.ListOfStringConvertedToDelimitedString); + Assert.Equal(ee.ValueConvertedArrayOfEnum, ee.ValueConvertedArrayOfEnum); + Assert.Equal(ee.ValueConvertedListOfEnum, ee.ValueConvertedListOfEnum); Assert.Equal(ee.IList, ee.IList); Assert.Equal(ee.Byte, ee.Byte); } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 0bc0b3508..1951cd85e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -25,15 +25,14 @@ public void Roundtrip() var x = ctx.SomeEntities.Single(e => e.Id == 1); Assert.Equal(new[] { 3, 4 }, x.IntArray); - Assert.Equal(new List { 3, 4 }, x.IntList); - Assert.Equal(new int?[] { 3, 4, null }, x.NullableIntArray); + Assert.Equal([3, 4], x.IntList); + Assert.Equal([3, 4, null], x.NullableIntArray); Assert.Equal( - new List - { - 3, - 4, - null - }, x.NullableIntList); + [ + 3, + 4, + null + ], x.NullableIntList); } #endregion @@ -504,12 +503,20 @@ public virtual Task Concat(bool async) // Note: see NorthwindFunctionsQueryNpgsqlTest.String_Join_non_aggregate for regular use without an array column/parameter [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task String_Join_with_array_parameter(bool async) + public virtual Task String_Join_with_array_of_int_column(bool async) => AssertQuery( async, ss => ss.Set() .Where(e => string.Join(", ", e.IntArray) == "3, 4")); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task String_Join_with_array_of_string_column(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task String_Join_disallow_non_array_type_mapped_parameter(bool async); + #endregion Other translations #region Support diff --git a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs index e48fcd9bd..72950440a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs @@ -128,19 +128,14 @@ await AssertQuery( private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class BigIntegerQueryContext : PoolableDbContext + public class BigIntegerQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet Entities { get; set; } - public BigIntegerQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(BigIntegerQueryContext context) + public static async Task SeedAsync(BigIntegerQueryContext context) { context.Entities.AddRange(BigIntegerData.CreateEntities()); - context.SaveChanges(); + await context.SaveChangesAsync(); } } @@ -150,7 +145,7 @@ public class Entity public BigInteger BigInteger { get; set; } } - public class BigIntegerQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase + public class BigIntegerQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory { private BigIntegerData _expectedData; @@ -163,8 +158,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(BigIntegerQueryContext context) - => BigIntegerQueryContext.Seed(context); + protected override Task SeedAsync(BigIntegerQueryContext context) + => BigIntegerQueryContext.SeedAsync(context); public Func GetContextCreator() => CreateContext; @@ -198,12 +193,7 @@ public IReadOnlyDictionary EntityAsserters protected class BigIntegerData : ISetSource { - public IReadOnlyList Entities { get; } - - public BigIntegerData() - { - Entities = CreateEntities(); - } + public IReadOnlyList Entities { get; } = CreateEntities(); public IQueryable Set() where TEntity : class diff --git a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs index 646ee13c7..d87ad0c9c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs @@ -44,11 +44,11 @@ public void StartsWith_param_pattern() Assert.Equal(1, result.Id); AssertSql( """ -@__param_0_rewritten='some%' +@__param_0_startswith='some%' SELECT s."Id", s."CaseInsensitiveText" FROM "SomeEntities" AS s -WHERE s."CaseInsensitiveText" LIKE @__param_0_rewritten ESCAPE '\' +WHERE s."CaseInsensitiveText" LIKE @__param_0_startswith LIMIT 2 """); } @@ -98,11 +98,11 @@ public void EndsWith_param_pattern() Assert.Equal(1, result.Id); AssertSql( """ -@__param_0_rewritten='%sometext' +@__param_0_endswith='%sometext' SELECT s."Id", s."CaseInsensitiveText" FROM "SomeEntities" AS s -WHERE s."CaseInsensitiveText" LIKE @__param_0_rewritten ESCAPE '\' +WHERE s."CaseInsensitiveText" LIKE @__param_0_endswith LIMIT 2 """); } @@ -152,11 +152,11 @@ public void Contains_param_pattern() Assert.Equal(1, result.Id); AssertSql( """ -@__param_0_rewritten='%ometex%' +@__param_0_contains='%ometex%' SELECT s."Id", s."CaseInsensitiveText" FROM "SomeEntities" AS s -WHERE s."CaseInsensitiveText" LIKE @__param_0_rewritten ESCAPE '\' +WHERE s."CaseInsensitiveText" LIKE @__param_0_contains LIMIT 2 """); } @@ -294,21 +294,16 @@ protected CitextQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class CitextQueryContext : PoolableDbContext + public class CitextQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet SomeEntities { get; set; } - public CitextQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(CitextQueryContext context) + public static async Task SeedAsync(CitextQueryContext context) { context.SomeEntities.AddRange( new SomeArrayEntity { Id = 1, CaseInsensitiveText = "SomeText" }, new SomeArrayEntity { Id = 2, CaseInsensitiveText = "AnotherText" }); - context.SaveChanges(); + await context.SaveChangesAsync(); } } @@ -331,7 +326,7 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(CitextQueryContext context) - => CitextQueryContext.Seed(context); + protected override Task SeedAsync(CitextQueryContext context) + => CitextQueryContext.SeedAsync(context); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs index ccf797ecd..0e20d87fc 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs @@ -73,11 +73,10 @@ public virtual CompatibilityContext CreateRedshiftContext() return new CompatibilityContext(builder.Options); } - public virtual Task InitializeAsync() + public virtual async Task InitializeAsync() { _testStore = NpgsqlTestStoreFactory.Instance.GetOrCreate(StoreName); - _testStore.Initialize(null, CreateContext, c => CompatibilityContext.Seed((CompatibilityContext)c)); - return Task.CompletedTask; + await _testStore.InitializeAsync(null, CreateContext, c => CompatibilityContext.SeedAsync((CompatibilityContext)c)); } // Called after DisposeAsync @@ -85,8 +84,8 @@ public virtual void Dispose() { } - public virtual Task DisposeAsync() - => _testStore.DisposeAsync(); + public virtual async Task DisposeAsync() + => await _testStore.DisposeAsync(); } public class CompatibilityTestEntity @@ -95,21 +94,16 @@ public class CompatibilityTestEntity public int SomeInt { get; set; } } - public class CompatibilityContext : DbContext + public class CompatibilityContext(DbContextOptions options) : DbContext(options) { public DbSet TestEntities { get; set; } - public CompatibilityContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(CompatibilityContext context) + public static async Task SeedAsync(CompatibilityContext context) { context.TestEntities.AddRange( new CompatibilityTestEntity { Id = 1, SomeInt = 8 }, new CompatibilityTestEntity { Id = 2, SomeInt = 10 }); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs index 6ec76cd82..ae27cc581 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs @@ -10,10 +10,6 @@ public ComplexNavigationsQueryNpgsqlTest(ComplexNavigationsQueryNpgsqlFixture fi Fixture.TestSqlLoggerFactory.Clear(); } - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26353")] - public override Task Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy(bool async) - => base.Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy(async); - public override async Task Join_with_result_selector_returning_queryable_throws_validation_error(bool async) => await Assert.ThrowsAsync( () => base.Join_with_result_selector_returning_queryable_throws_validation_error(async)); diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs index 7626042e8..69e20028d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs @@ -19,14 +19,6 @@ public ComplexNavigationsSharedTypeQueryNpgsqlTest( public override Task Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy(bool async) => base.Subquery_with_Distinct_Skip_FirstOrDefault_without_OrderBy(async); - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/22532")] - public override Task Distinct_skip_without_orderby(bool async) - => base.Distinct_skip_without_orderby(async); - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/22532")] - public override Task Distinct_take_without_orderby(bool async) - => base.Distinct_take_without_orderby(async); - public override async Task Join_with_result_selector_returning_queryable_throws_validation_error(bool async) => await Assert.ThrowsAsync( () => base.Join_with_result_selector_returning_queryable_throws_validation_error(async)); diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs index 1d4e555bd..d46b14a82 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs @@ -18,7 +18,7 @@ public override async Task Filter_on_property_inside_complex_type(bool async) AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."ShippingAddress_ZipCode" = 7728 """); @@ -30,7 +30,7 @@ public override async Task Filter_on_property_inside_nested_complex_type(bool as AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."ShippingAddress_Country_Code" = 'DE' """); @@ -44,14 +44,14 @@ public override async Task Filter_on_property_inside_complex_type_after_subquery """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM ( - SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c ORDER BY c."Id" NULLS FIRST OFFSET @__p_0 -) AS t -WHERE t."ShippingAddress_ZipCode" = 7728 +) AS c0 +WHERE c0."ShippingAddress_ZipCode" = 7728 """); } @@ -63,14 +63,14 @@ public override async Task Filter_on_property_inside_nested_complex_type_after_s """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM ( - SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c ORDER BY c."Id" NULLS FIRST OFFSET @__p_0 -) AS t -WHERE t."ShippingAddress_Country_Code" = 'DE' +) AS c0 +WHERE c0."ShippingAddress_Country_Code" = 'DE' """); } @@ -80,7 +80,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT c."Id", c."OptionalCustomerId", c."RequiredCustomerId", c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName", c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName" +SELECT c."Id", c."OptionalCustomerId", c."RequiredCustomerId", c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName", c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_Tags", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS c LEFT JOIN "Customer" AS c0 ON c."OptionalCustomerId" = c0."Id" INNER JOIN "Customer" AS c1 ON c."RequiredCustomerId" = c1."Id" @@ -94,7 +94,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT c."Id", c."OptionalCustomerId", c."RequiredCustomerId", c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName", c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c."Id", c."OptionalCustomerId", c."RequiredCustomerId", c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_Tags", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName", c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS c INNER JOIN "Customer" AS c0 ON c."RequiredCustomerId" = c0."Id" LEFT JOIN "Customer" AS c1 ON c."OptionalCustomerId" = c1."Id" @@ -118,7 +118,7 @@ public override async Task Project_complex_type_via_required_navigation(bool asy AssertSql( """ -SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS c INNER JOIN "Customer" AS c0 ON c."RequiredCustomerId" = c0."Id" """); @@ -132,13 +132,13 @@ public override async Task Load_complex_type_after_subquery_on_entity_type(bool """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM ( - SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c ORDER BY c."Id" NULLS FIRST OFFSET @__p_0 -) AS t +) AS c0 """); } @@ -148,7 +148,7 @@ public override async Task Select_complex_type(bool async) AssertSql( """ -SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c """); } @@ -181,7 +181,7 @@ public override async Task Select_complex_type_Where(bool async) AssertSql( """ -SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."ShippingAddress_ZipCode" = 7728 """); @@ -193,7 +193,7 @@ public override async Task Select_complex_type_Distinct(bool async) AssertSql( """ -SELECT DISTINCT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT DISTINCT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c """); } @@ -204,9 +204,9 @@ public override async Task Complex_type_equals_complex_type(bool async) AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c -WHERE c."ShippingAddress_AddressLine1" = c."BillingAddress_AddressLine1" AND (c."ShippingAddress_AddressLine2" = c."BillingAddress_AddressLine2" OR (c."ShippingAddress_AddressLine2" IS NULL AND c."BillingAddress_AddressLine2" IS NULL)) AND c."ShippingAddress_ZipCode" = c."BillingAddress_ZipCode" +WHERE c."ShippingAddress_AddressLine1" = c."BillingAddress_AddressLine1" AND (c."ShippingAddress_AddressLine2" = c."BillingAddress_AddressLine2" OR (c."ShippingAddress_AddressLine2" IS NULL AND c."BillingAddress_AddressLine2" IS NULL)) AND c."ShippingAddress_Tags" = c."BillingAddress_Tags" AND c."ShippingAddress_ZipCode" = c."BillingAddress_ZipCode" """); } @@ -216,9 +216,9 @@ public override async Task Complex_type_equals_constant(bool async) AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c -WHERE c."ShippingAddress_AddressLine1" = '804 S. Lakeshore Road' AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_ZipCode" = 38654 AND c."ShippingAddress_Country_Code" = 'US' AND c."ShippingAddress_Country_FullName" = 'United States' +WHERE c."ShippingAddress_AddressLine1" = '804 S. Lakeshore Road' AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_Tags" = ARRAY['foo','bar']::text[] AND c."ShippingAddress_ZipCode" = 38654 AND c."ShippingAddress_Country_Code" = 'US' AND c."ShippingAddress_Country_FullName" = 'United States' """); } @@ -229,13 +229,14 @@ public override async Task Complex_type_equals_parameter(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' +@__entity_equality_address_0_Tags={ 'foo', 'bar' } (DbType = Object) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) -@__entity_equality_address_0_Code='US' -@__entity_equality_address_0_FullName='United States' +@__entity_equality_address_0_Country_Code='US' +@__entity_equality_address_0_Country_FullName='United States' -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c -WHERE c."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND c."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND c."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName +WHERE c."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_Tags" = @__entity_equality_address_0_Tags AND c."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND c."ShippingAddress_Country_Code" = @__entity_equality_address_0_Country_Code AND c."ShippingAddress_Country_FullName" = @__entity_equality_address_0_Country_FullName """); } @@ -260,16 +261,17 @@ public override async Task Contains_over_complex_type(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' +@__entity_equality_address_0_Tags={ 'foo', 'bar' } (DbType = Object) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) -@__entity_equality_address_0_Code='US' -@__entity_equality_address_0_FullName='United States' +@__entity_equality_address_0_Country_Code='US' +@__entity_equality_address_0_Country_FullName='United States' -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE EXISTS ( SELECT 1 FROM "Customer" AS c0 - WHERE c0."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND c0."ShippingAddress_AddressLine2" IS NULL AND c0."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND c0."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND c0."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName) + WHERE c0."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND c0."ShippingAddress_AddressLine2" IS NULL AND c0."ShippingAddress_Tags" = @__entity_equality_address_0_Tags AND c0."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND c0."ShippingAddress_Country_Code" = @__entity_equality_address_0_Country_Code AND c0."ShippingAddress_Country_FullName" = @__entity_equality_address_0_Country_FullName) """); } @@ -279,11 +281,11 @@ public override async Task Concat_complex_type(bool async) AssertSql( """ -SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."Id" = 1 UNION ALL -SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "Customer" AS c0 WHERE c0."Id" = 2 """); @@ -295,11 +297,11 @@ public override async Task Concat_entity_type_containing_complex_property(bool a AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."Id" = 1 UNION ALL -SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "Customer" AS c0 WHERE c0."Id" = 2 """); @@ -311,11 +313,11 @@ public override async Task Union_entity_type_containing_complex_property(bool as AssertSql( """ -SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."Id" = 1 UNION -SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "Customer" AS c0 WHERE c0."Id" = 2 """); @@ -327,11 +329,11 @@ public override async Task Union_complex_type(bool async) AssertSql( """ -SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" FROM "Customer" AS c WHERE c."Id" = 1 UNION -SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" +SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" FROM "Customer" AS c0 WHERE c0."Id" = 2 """); @@ -411,14 +413,14 @@ public override async Task Filter_on_property_inside_struct_complex_type_after_s """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT v0."Id", v0."Name", v0."BillingAddress_AddressLine1", v0."BillingAddress_AddressLine2", v0."BillingAddress_ZipCode", v0."BillingAddress_Country_Code", v0."BillingAddress_Country_FullName", v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" FROM ( SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" FROM "ValuedCustomer" AS v ORDER BY v."Id" NULLS FIRST OFFSET @__p_0 -) AS t -WHERE t."ShippingAddress_ZipCode" = 7728 +) AS v0 +WHERE v0."ShippingAddress_ZipCode" = 7728 """); } @@ -430,14 +432,14 @@ public override async Task Filter_on_property_inside_nested_struct_complex_type_ """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT v0."Id", v0."Name", v0."BillingAddress_AddressLine1", v0."BillingAddress_AddressLine2", v0."BillingAddress_ZipCode", v0."BillingAddress_Country_Code", v0."BillingAddress_Country_FullName", v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" FROM ( SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" FROM "ValuedCustomer" AS v ORDER BY v."Id" NULLS FIRST OFFSET @__p_0 -) AS t -WHERE t."ShippingAddress_Country_Code" = 'DE' +) AS v0 +WHERE v0."ShippingAddress_Country_Code" = 'DE' """); } @@ -500,13 +502,13 @@ public override async Task Load_struct_complex_type_after_subquery_on_entity_typ """ @__p_0='1' -SELECT DISTINCT t."Id", t."Name", t."BillingAddress_AddressLine1", t."BillingAddress_AddressLine2", t."BillingAddress_ZipCode", t."BillingAddress_Country_Code", t."BillingAddress_Country_FullName", t."ShippingAddress_AddressLine1", t."ShippingAddress_AddressLine2", t."ShippingAddress_ZipCode", t."ShippingAddress_Country_Code", t."ShippingAddress_Country_FullName" +SELECT DISTINCT v0."Id", v0."Name", v0."BillingAddress_AddressLine1", v0."BillingAddress_AddressLine2", v0."BillingAddress_ZipCode", v0."BillingAddress_Country_Code", v0."BillingAddress_Country_FullName", v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" FROM ( SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" FROM "ValuedCustomer" AS v ORDER BY v."Id" NULLS FIRST OFFSET @__p_0 -) AS t +) AS v0 """); } @@ -598,12 +600,12 @@ public override async Task Struct_complex_type_equals_parameter(bool async) """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' @__entity_equality_address_0_ZipCode='38654' (Nullable = true) -@__entity_equality_address_0_Code='US' -@__entity_equality_address_0_FullName='United States' +@__entity_equality_address_0_Country_Code='US' +@__entity_equality_address_0_Country_FullName='United States' SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" FROM "ValuedCustomer" AS v -WHERE v."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND v."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND v."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName +WHERE v."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND v."ShippingAddress_Country_Code" = @__entity_equality_address_0_Country_Code AND v."ShippingAddress_Country_FullName" = @__entity_equality_address_0_Country_FullName """); } @@ -622,15 +624,15 @@ public override async Task Contains_over_struct_complex_type(bool async) """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' @__entity_equality_address_0_ZipCode='38654' (Nullable = true) -@__entity_equality_address_0_Code='US' -@__entity_equality_address_0_FullName='United States' +@__entity_equality_address_0_Country_Code='US' +@__entity_equality_address_0_Country_FullName='United States' SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" FROM "ValuedCustomer" AS v WHERE EXISTS ( SELECT 1 FROM "ValuedCustomer" AS v0 - WHERE v0."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND v0."ShippingAddress_AddressLine2" IS NULL AND v0."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND v0."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND v0."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName) + WHERE v0."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND v0."ShippingAddress_AddressLine2" IS NULL AND v0."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND v0."ShippingAddress_Country_Code" = @__entity_equality_address_0_Country_Code AND v0."ShippingAddress_Country_FullName" = @__entity_equality_address_0_Country_FullName) """); } @@ -740,6 +742,418 @@ public override async Task Union_two_different_struct_complex_type(bool async) AssertSql(); } + public override async Task Project_same_entity_with_nested_complex_type_twice_with_pushdown(bool async) + { + await base.Project_same_entity_with_nested_complex_type_twice_with_pushdown(async); + + AssertSql( +""" +SELECT s."Id", s."Name", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_Tags", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName", s."Id0", s."Name0", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s."ShippingAddress_AddressLine10", s."ShippingAddress_AddressLine20", s."ShippingAddress_Tags0", s."ShippingAddress_ZipCode0", s."ShippingAddress_Country_Code0", s."ShippingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", c0."Id" AS "Id0", c0."Name" AS "Name0", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c0."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 +) AS s +"""); + } + + public override async Task Project_same_nested_complex_type_twice_with_pushdown(bool async) + { + await base.Project_same_nested_complex_type_twice_with_pushdown(async); + + AssertSql( + """ +SELECT s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 +) AS s +"""); + } + + public override async Task Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(bool async) + { + await base.Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT s0."Id", s0."Name", s0."BillingAddress_AddressLine1", s0."BillingAddress_AddressLine2", s0."BillingAddress_Tags", s0."BillingAddress_ZipCode", s0."BillingAddress_Country_Code", s0."BillingAddress_Country_FullName", s0."ShippingAddress_AddressLine1", s0."ShippingAddress_AddressLine2", s0."ShippingAddress_Tags", s0."ShippingAddress_ZipCode", s0."ShippingAddress_Country_Code", s0."ShippingAddress_Country_FullName", s0."Id0", s0."Name0", s0."BillingAddress_AddressLine10", s0."BillingAddress_AddressLine20", s0."BillingAddress_Tags0", s0."BillingAddress_ZipCode0", s0."BillingAddress_Country_Code0", s0."BillingAddress_Country_FullName0", s0."ShippingAddress_AddressLine10", s0."ShippingAddress_AddressLine20", s0."ShippingAddress_Tags0", s0."ShippingAddress_ZipCode0", s0."ShippingAddress_Country_Code0", s0."ShippingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT s."Id", s."Name", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_Tags", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName", s."Id0", s."Name0", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s."ShippingAddress_AddressLine10", s."ShippingAddress_AddressLine20", s."ShippingAddress_Tags0", s."ShippingAddress_ZipCode0", s."ShippingAddress_Country_Code0", s."ShippingAddress_Country_FullName0" + FROM ( + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", c0."Id" AS "Id0", c0."Name" AS "Name0", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c0."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + ORDER BY c."Id" NULLS FIRST, c0."Id" NULLS FIRST + LIMIT @__p_0 + ) AS s +) AS s0 +"""); + } + + public override async Task Project_same_nested_complex_type_twice_with_double_pushdown(bool async) + { + await base.Project_same_nested_complex_type_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT s0."BillingAddress_AddressLine1", s0."BillingAddress_AddressLine2", s0."BillingAddress_Tags", s0."BillingAddress_ZipCode", s0."BillingAddress_Country_Code", s0."BillingAddress_Country_FullName", s0."BillingAddress_AddressLine10", s0."BillingAddress_AddressLine20", s0."BillingAddress_Tags0", s0."BillingAddress_ZipCode0", s0."BillingAddress_Country_Code0", s0."BillingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0" + FROM ( + SELECT c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + ORDER BY c."Id" NULLS FIRST, c0."Id" NULLS FIRST + LIMIT @__p_0 + ) AS s +) AS s0 +"""); + } + + public override async Task Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(bool async) + { + await base.Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(async); + + AssertSql( + """ +SELECT s."Id", s."Name", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName", s."Id0", s."Name0", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s."ShippingAddress_AddressLine10", s."ShippingAddress_AddressLine20", s."ShippingAddress_ZipCode0", s."ShippingAddress_Country_Code0", s."ShippingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName", v0."Id" AS "Id0", v0."Name" AS "Name0", v0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", v0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", v0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", v0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", v0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", v0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", v0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", v0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", v0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", v0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "ValuedCustomer" AS v + CROSS JOIN "ValuedCustomer" AS v0 +) AS s +"""); + } + + public override async Task Project_same_struct_nested_complex_type_twice_with_pushdown(bool async) + { + await base.Project_same_struct_nested_complex_type_twice_with_pushdown(async); + + AssertSql( + """ +SELECT s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", v0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", v0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", v0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", v0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "ValuedCustomer" AS v + CROSS JOIN "ValuedCustomer" AS v0 +) AS s +"""); + } + + public override async Task Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(bool async) + { + await base.Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT s0."Id", s0."Name", s0."BillingAddress_AddressLine1", s0."BillingAddress_AddressLine2", s0."BillingAddress_ZipCode", s0."BillingAddress_Country_Code", s0."BillingAddress_Country_FullName", s0."ShippingAddress_AddressLine1", s0."ShippingAddress_AddressLine2", s0."ShippingAddress_ZipCode", s0."ShippingAddress_Country_Code", s0."ShippingAddress_Country_FullName", s0."Id0", s0."Name0", s0."BillingAddress_AddressLine10", s0."BillingAddress_AddressLine20", s0."BillingAddress_ZipCode0", s0."BillingAddress_Country_Code0", s0."BillingAddress_Country_FullName0", s0."ShippingAddress_AddressLine10", s0."ShippingAddress_AddressLine20", s0."ShippingAddress_ZipCode0", s0."ShippingAddress_Country_Code0", s0."ShippingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT s."Id", s."Name", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName", s."Id0", s."Name0", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s."ShippingAddress_AddressLine10", s."ShippingAddress_AddressLine20", s."ShippingAddress_ZipCode0", s."ShippingAddress_Country_Code0", s."ShippingAddress_Country_FullName0" + FROM ( + SELECT v."Id", v."Name", v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName", v0."Id" AS "Id0", v0."Name" AS "Name0", v0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", v0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", v0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", v0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", v0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", v0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", v0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", v0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", v0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", v0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "ValuedCustomer" AS v + CROSS JOIN "ValuedCustomer" AS v0 + ORDER BY v."Id" NULLS FIRST, v0."Id" NULLS FIRST + LIMIT @__p_0 + ) AS s +) AS s0 +"""); + } + + public override async Task Project_same_struct_nested_complex_type_twice_with_double_pushdown(bool async) + { + await base.Project_same_struct_nested_complex_type_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT s0."BillingAddress_AddressLine1", s0."BillingAddress_AddressLine2", s0."BillingAddress_ZipCode", s0."BillingAddress_Country_Code", s0."BillingAddress_Country_FullName", s0."BillingAddress_AddressLine10", s0."BillingAddress_AddressLine20", s0."BillingAddress_ZipCode0", s0."BillingAddress_Country_Code0", s0."BillingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0" + FROM ( + SELECT v."BillingAddress_AddressLine1", v."BillingAddress_AddressLine2", v."BillingAddress_ZipCode", v."BillingAddress_Country_Code", v."BillingAddress_Country_FullName", v0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", v0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", v0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", v0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", v0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "ValuedCustomer" AS v + CROSS JOIN "ValuedCustomer" AS v0 + ORDER BY v."Id" NULLS FIRST, v0."Id" NULLS FIRST + LIMIT @__p_0 + ) AS s +) AS s0 +"""); + } + + public override async Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(bool async) + { + await base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT u."Id", u."Name", u."BillingAddress_AddressLine1", u."BillingAddress_AddressLine2", u."BillingAddress_Tags", u."BillingAddress_ZipCode", u."BillingAddress_Country_Code", u."BillingAddress_Country_FullName", u."ShippingAddress_AddressLine1", u."ShippingAddress_AddressLine2", u."ShippingAddress_Tags", u."ShippingAddress_ZipCode", u."ShippingAddress_Country_Code", u."ShippingAddress_Country_FullName", u."Id0", u."Name0", u."BillingAddress_AddressLine10", u."BillingAddress_AddressLine20", u."BillingAddress_Tags0", u."BillingAddress_ZipCode0", u."BillingAddress_Country_Code0", u."BillingAddress_Country_FullName0", u."ShippingAddress_AddressLine10", u."ShippingAddress_AddressLine20", u."ShippingAddress_Tags0", u."ShippingAddress_ZipCode0", u."ShippingAddress_Country_Code0", u."ShippingAddress_Country_FullName0" +FROM ( + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", c0."Id" AS "Id0", c0."Name" AS "Name0", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c0."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + UNION + SELECT c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_Tags", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName", c2."Id" AS "Id0", c2."Name" AS "Name0", c2."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c2."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c2."BillingAddress_Tags" AS "BillingAddress_Tags0", c2."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c2."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c2."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c2."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c2."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c2."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c2."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c2."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c2."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c1 + CROSS JOIN "Customer" AS c2 +) AS u +ORDER BY u."Id" NULLS FIRST, u."Id0" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(bool async) + { + await base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT u1."Id", u1."Name", u1."BillingAddress_AddressLine1", u1."BillingAddress_AddressLine2", u1."BillingAddress_Tags", u1."BillingAddress_ZipCode", u1."BillingAddress_Country_Code", u1."BillingAddress_Country_FullName", u1."ShippingAddress_AddressLine1", u1."ShippingAddress_AddressLine2", u1."ShippingAddress_Tags", u1."ShippingAddress_ZipCode", u1."ShippingAddress_Country_Code", u1."ShippingAddress_Country_FullName", u1."Id0", u1."Name0", u1."BillingAddress_AddressLine10", u1."BillingAddress_AddressLine20", u1."BillingAddress_Tags0", u1."BillingAddress_ZipCode0", u1."BillingAddress_Country_Code0", u1."BillingAddress_Country_FullName0", u1."ShippingAddress_AddressLine10", u1."ShippingAddress_AddressLine20", u1."ShippingAddress_Tags0", u1."ShippingAddress_ZipCode0", u1."ShippingAddress_Country_Code0", u1."ShippingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT u0."Id", u0."Name", u0."BillingAddress_AddressLine1", u0."BillingAddress_AddressLine2", u0."BillingAddress_Tags", u0."BillingAddress_ZipCode", u0."BillingAddress_Country_Code", u0."BillingAddress_Country_FullName", u0."ShippingAddress_AddressLine1", u0."ShippingAddress_AddressLine2", u0."ShippingAddress_Tags", u0."ShippingAddress_ZipCode", u0."ShippingAddress_Country_Code", u0."ShippingAddress_Country_FullName", u0."Id0", u0."Name0", u0."BillingAddress_AddressLine10", u0."BillingAddress_AddressLine20", u0."BillingAddress_Tags0", u0."BillingAddress_ZipCode0", u0."BillingAddress_Country_Code0", u0."BillingAddress_Country_FullName0", u0."ShippingAddress_AddressLine10", u0."ShippingAddress_AddressLine20", u0."ShippingAddress_Tags0", u0."ShippingAddress_ZipCode0", u0."ShippingAddress_Country_Code0", u0."ShippingAddress_Country_FullName0" + FROM ( + SELECT u."Id", u."Name", u."BillingAddress_AddressLine1", u."BillingAddress_AddressLine2", u."BillingAddress_Tags", u."BillingAddress_ZipCode", u."BillingAddress_Country_Code", u."BillingAddress_Country_FullName", u."ShippingAddress_AddressLine1", u."ShippingAddress_AddressLine2", u."ShippingAddress_Tags", u."ShippingAddress_ZipCode", u."ShippingAddress_Country_Code", u."ShippingAddress_Country_FullName", u."Id0", u."Name0", u."BillingAddress_AddressLine10", u."BillingAddress_AddressLine20", u."BillingAddress_Tags0", u."BillingAddress_ZipCode0", u."BillingAddress_Country_Code0", u."BillingAddress_Country_FullName0", u."ShippingAddress_AddressLine10", u."ShippingAddress_AddressLine20", u."ShippingAddress_Tags0", u."ShippingAddress_ZipCode0", u."ShippingAddress_Country_Code0", u."ShippingAddress_Country_FullName0" + FROM ( + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", c0."Id" AS "Id0", c0."Name" AS "Name0", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c0."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c0."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c0."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c0."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c0."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c0."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + UNION + SELECT c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_Tags", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName", c2."Id" AS "Id0", c2."Name" AS "Name0", c2."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c2."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c2."BillingAddress_Tags" AS "BillingAddress_Tags0", c2."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c2."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c2."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c2."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c2."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c2."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c2."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c2."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c2."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0" + FROM "Customer" AS c1 + CROSS JOIN "Customer" AS c2 + ) AS u + ORDER BY u."Id" NULLS FIRST, u."Id0" NULLS FIRST + LIMIT @__p_0 + ) AS u0 +) AS u1 +ORDER BY u1."Id" NULLS FIRST, u1."Id0" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Union_of_same_nested_complex_type_projected_twice_with_pushdown(bool async) + { + await base.Union_of_same_nested_complex_type_projected_twice_with_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT u."BillingAddress_AddressLine1", u."BillingAddress_AddressLine2", u."BillingAddress_Tags", u."BillingAddress_ZipCode", u."BillingAddress_Country_Code", u."BillingAddress_Country_FullName", u."BillingAddress_AddressLine10", u."BillingAddress_AddressLine20", u."BillingAddress_Tags0", u."BillingAddress_ZipCode0", u."BillingAddress_Country_Code0", u."BillingAddress_Country_FullName0" +FROM ( + SELECT c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + UNION + SELECT c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c2."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c2."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c2."BillingAddress_Tags" AS "BillingAddress_Tags0", c2."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c2."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c2."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c1 + CROSS JOIN "Customer" AS c2 +) AS u +ORDER BY u."BillingAddress_ZipCode" NULLS FIRST, u."BillingAddress_ZipCode0" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(bool async) + { + await base.Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(async); + + AssertSql( + """ +@__p_0='50' + +SELECT u1."BillingAddress_AddressLine1", u1."BillingAddress_AddressLine2", u1."BillingAddress_Tags", u1."BillingAddress_ZipCode", u1."BillingAddress_Country_Code", u1."BillingAddress_Country_FullName", u1."BillingAddress_AddressLine10", u1."BillingAddress_AddressLine20", u1."BillingAddress_Tags0", u1."BillingAddress_ZipCode0", u1."BillingAddress_Country_Code0", u1."BillingAddress_Country_FullName0" +FROM ( + SELECT DISTINCT u0."BillingAddress_AddressLine1", u0."BillingAddress_AddressLine2", u0."BillingAddress_Tags", u0."BillingAddress_ZipCode", u0."BillingAddress_Country_Code", u0."BillingAddress_Country_FullName", u0."BillingAddress_AddressLine10", u0."BillingAddress_AddressLine20", u0."BillingAddress_Tags0", u0."BillingAddress_ZipCode0", u0."BillingAddress_Country_Code0", u0."BillingAddress_Country_FullName0" + FROM ( + SELECT u."BillingAddress_AddressLine1", u."BillingAddress_AddressLine2", u."BillingAddress_Tags", u."BillingAddress_ZipCode", u."BillingAddress_Country_Code", u."BillingAddress_Country_FullName", u."BillingAddress_AddressLine10", u."BillingAddress_AddressLine20", u."BillingAddress_Tags0", u."BillingAddress_ZipCode0", u."BillingAddress_Country_Code0", u."BillingAddress_Country_FullName0" + FROM ( + SELECT c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c0."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c0."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c0."BillingAddress_Tags" AS "BillingAddress_Tags0", c0."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c0."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c0."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c + CROSS JOIN "Customer" AS c0 + UNION + SELECT c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c2."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c2."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c2."BillingAddress_Tags" AS "BillingAddress_Tags0", c2."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c2."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c2."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0" + FROM "Customer" AS c1 + CROSS JOIN "Customer" AS c2 + ) AS u + ORDER BY u."BillingAddress_ZipCode" NULLS FIRST, u."BillingAddress_ZipCode0" NULLS FIRST + LIMIT @__p_0 + ) AS u0 +) AS u1 +ORDER BY u1."BillingAddress_ZipCode" NULLS FIRST, u1."BillingAddress_ZipCode0" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) + { + await base.Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async); + + AssertSql( + """ +SELECT c."Id", s."Id", s."Name", s."BillingAddress_AddressLine1", s."BillingAddress_AddressLine2", s."BillingAddress_Tags", s."BillingAddress_ZipCode", s."BillingAddress_Country_Code", s."BillingAddress_Country_FullName", s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_Tags", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName", s."Id0", s."Name0", s."BillingAddress_AddressLine10", s."BillingAddress_AddressLine20", s."BillingAddress_Tags0", s."BillingAddress_ZipCode0", s."BillingAddress_Country_Code0", s."BillingAddress_Country_FullName0", s."ShippingAddress_AddressLine10", s."ShippingAddress_AddressLine20", s."ShippingAddress_Tags0", s."ShippingAddress_ZipCode0", s."ShippingAddress_Country_Code0", s."ShippingAddress_Country_FullName0", s.c +FROM "Customer" AS c +LEFT JOIN LATERAL ( + SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName", c1."Id" AS "Id0", c1."Name" AS "Name0", c1."BillingAddress_AddressLine1" AS "BillingAddress_AddressLine10", c1."BillingAddress_AddressLine2" AS "BillingAddress_AddressLine20", c1."BillingAddress_Tags" AS "BillingAddress_Tags0", c1."BillingAddress_ZipCode" AS "BillingAddress_ZipCode0", c1."BillingAddress_Country_Code" AS "BillingAddress_Country_Code0", c1."BillingAddress_Country_FullName" AS "BillingAddress_Country_FullName0", c1."ShippingAddress_AddressLine1" AS "ShippingAddress_AddressLine10", c1."ShippingAddress_AddressLine2" AS "ShippingAddress_AddressLine20", c1."ShippingAddress_Tags" AS "ShippingAddress_Tags0", c1."ShippingAddress_ZipCode" AS "ShippingAddress_ZipCode0", c1."ShippingAddress_Country_Code" AS "ShippingAddress_Country_Code0", c1."ShippingAddress_Country_FullName" AS "ShippingAddress_Country_FullName0", 1 AS c + FROM "Customer" AS c0 + CROSS JOIN "Customer" AS c1 + ORDER BY c0."Id" NULLS FIRST, c1."Id" DESC NULLS LAST + LIMIT 1 +) AS s ON TRUE +"""); + } + + public override async Task Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) + { + await base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async); + + AssertSql(""); + } + + #region GroupBy + + public override async Task GroupBy_over_property_in_nested_complex_type(bool async) + { + await base.GroupBy_over_property_in_nested_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_Country_Code" AS "Code", count(*)::int AS "Count" +FROM "Customer" AS c +GROUP BY c."ShippingAddress_Country_Code" +"""); + } + + public override async Task GroupBy_over_complex_type(bool async) + { + await base.GroupBy_over_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", count(*)::int AS "Count" +FROM "Customer" AS c +GROUP BY c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +"""); + } + + public override async Task GroupBy_over_nested_complex_type(bool async) + { + await base.GroupBy_over_nested_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName", count(*)::int AS "Count" +FROM "Customer" AS c +GROUP BY c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +"""); + } + + public override async Task Entity_with_complex_type_with_group_by_and_first(bool async) + { + await base.Entity_with_complex_type_with_group_by_and_first(async); + + AssertSql( + """ +SELECT c3."Id", c3."Name", c3."BillingAddress_AddressLine1", c3."BillingAddress_AddressLine2", c3."BillingAddress_Tags", c3."BillingAddress_ZipCode", c3."BillingAddress_Country_Code", c3."BillingAddress_Country_FullName", c3."ShippingAddress_AddressLine1", c3."ShippingAddress_AddressLine2", c3."ShippingAddress_Tags", c3."ShippingAddress_ZipCode", c3."ShippingAddress_Country_Code", c3."ShippingAddress_Country_FullName" +FROM ( + SELECT c."Id" + FROM "Customer" AS c + GROUP BY c."Id" +) AS c1 +LEFT JOIN ( + SELECT c2."Id", c2."Name", c2."BillingAddress_AddressLine1", c2."BillingAddress_AddressLine2", c2."BillingAddress_Tags", c2."BillingAddress_ZipCode", c2."BillingAddress_Country_Code", c2."BillingAddress_Country_FullName", c2."ShippingAddress_AddressLine1", c2."ShippingAddress_AddressLine2", c2."ShippingAddress_Tags", c2."ShippingAddress_ZipCode", c2."ShippingAddress_Country_Code", c2."ShippingAddress_Country_FullName" + FROM ( + SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName", ROW_NUMBER() OVER(PARTITION BY c0."Id" ORDER BY c0."Id" NULLS FIRST) AS row + FROM "Customer" AS c0 + ) AS c2 + WHERE c2.row <= 1 +) AS c3 ON c1."Id" = c3."Id" +"""); + } + + #endregion GroupBy + + public override async Task Projecting_property_of_complex_type_using_left_join_with_pushdown(bool async) + { + await base.Projecting_property_of_complex_type_using_left_join_with_pushdown(async); + + AssertSql( + """ +SELECT c1."BillingAddress_ZipCode" +FROM "CustomerGroup" AS c +LEFT JOIN ( + SELECT c0."Id", c0."BillingAddress_ZipCode" + FROM "Customer" AS c0 + WHERE c0."Id" > 5 +) AS c1 ON c."Id" = c1."Id" +"""); + } + + public override async Task Projecting_complex_from_optional_navigation_using_conditional(bool async) + { + await base.Projecting_complex_from_optional_navigation_using_conditional(async); + + AssertSql( + """ +@__p_0='20' + +SELECT s0."ShippingAddress_ZipCode" +FROM ( + SELECT DISTINCT s."ShippingAddress_AddressLine1", s."ShippingAddress_AddressLine2", s."ShippingAddress_Tags", s."ShippingAddress_ZipCode", s."ShippingAddress_Country_Code", s."ShippingAddress_Country_FullName" + FROM ( + SELECT c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" + FROM "CustomerGroup" AS c + LEFT JOIN "Customer" AS c0 ON c."OptionalCustomerId" = c0."Id" + ORDER BY c0."ShippingAddress_ZipCode" NULLS FIRST + LIMIT @__p_0 + ) AS s +) AS s0 +"""); } + + public override async Task Project_entity_with_complex_type_pushdown_and_then_left_join(bool async) + { + await base.Project_entity_with_complex_type_pushdown_and_then_left_join(async); + + AssertSql( +""" +@__p_0='20' +@__p_1='30' + +SELECT c3."BillingAddress_ZipCode" AS "Zip1", c4."ShippingAddress_ZipCode" AS "Zip2" +FROM ( + SELECT DISTINCT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName" + FROM ( + SELECT c."Id", c."Name", c."BillingAddress_AddressLine1", c."BillingAddress_AddressLine2", c."BillingAddress_Tags", c."BillingAddress_ZipCode", c."BillingAddress_Country_Code", c."BillingAddress_Country_FullName", c."ShippingAddress_AddressLine1", c."ShippingAddress_AddressLine2", c."ShippingAddress_Tags", c."ShippingAddress_ZipCode", c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" + FROM "Customer" AS c + ORDER BY c."Id" NULLS FIRST + LIMIT @__p_0 + ) AS c0 +) AS c3 +LEFT JOIN ( + SELECT DISTINCT c2."Id", c2."Name", c2."BillingAddress_AddressLine1", c2."BillingAddress_AddressLine2", c2."BillingAddress_Tags", c2."BillingAddress_ZipCode", c2."BillingAddress_Country_Code", c2."BillingAddress_Country_FullName", c2."ShippingAddress_AddressLine1", c2."ShippingAddress_AddressLine2", c2."ShippingAddress_Tags", c2."ShippingAddress_ZipCode", c2."ShippingAddress_Country_Code", c2."ShippingAddress_Country_FullName" + FROM ( + SELECT c1."Id", c1."Name", c1."BillingAddress_AddressLine1", c1."BillingAddress_AddressLine2", c1."BillingAddress_Tags", c1."BillingAddress_ZipCode", c1."BillingAddress_Country_Code", c1."BillingAddress_Country_FullName", c1."ShippingAddress_AddressLine1", c1."ShippingAddress_AddressLine2", c1."ShippingAddress_Tags", c1."ShippingAddress_ZipCode", c1."ShippingAddress_Country_Code", c1."ShippingAddress_Country_FullName" + FROM "Customer" AS c1 + ORDER BY c1."Id" DESC NULLS LAST + LIMIT @__p_1 + ) AS c2 +) AS c4 ON c3."Id" = c4."Id" +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs index 141f111d5..2e7a5b46e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs @@ -18,17 +18,17 @@ public override async Task Whats_new_2021_sample_3(bool async) AssertSql( """ SELECT ( - SELECT p1."LastName" - FROM "Person" AS p1 - WHERE p1."MiddleInitial" = 'Q' AND p1."Age" = 20 AND (p."LastName" = p1."LastName" OR (p."LastName" IS NULL AND p1."LastName" IS NULL)) + SELECT p0."LastName" + FROM "Person" AS p0 + WHERE p0."MiddleInitial" = 'Q' AND p0."Age" = 20 AND (p."LastName" = p0."LastName" OR (p."LastName" IS NULL AND p0."LastName" IS NULL)) LIMIT 1) FROM "Person" AS p WHERE p."MiddleInitial" = 'Q' AND p."Age" = 20 GROUP BY p."LastName" ORDER BY length(( - SELECT p1."LastName" - FROM "Person" AS p1 - WHERE p1."MiddleInitial" = 'Q' AND p1."Age" = 20 AND (p."LastName" = p1."LastName" OR (p."LastName" IS NULL AND p1."LastName" IS NULL)) + SELECT p0."LastName" + FROM "Person" AS p0 + WHERE p0."MiddleInitial" = 'Q' AND p0."Age" = 20 AND (p."LastName" = p0."LastName" OR (p."LastName" IS NULL AND p0."LastName" IS NULL)) LIMIT 1))::int NULLS FIRST """); } @@ -40,16 +40,16 @@ public override async Task Whats_new_2021_sample_5(bool async) AssertSql( """ SELECT ( - SELECT p1."LastName" - FROM "Person" AS p1 - WHERE p."FirstName" = p1."FirstName" OR (p."FirstName" IS NULL AND p1."FirstName" IS NULL) + SELECT p0."LastName" + FROM "Person" AS p0 + WHERE p."FirstName" = p0."FirstName" OR (p."FirstName" IS NULL AND p0."FirstName" IS NULL) LIMIT 1) FROM "Person" AS p GROUP BY p."FirstName" ORDER BY ( - SELECT p1."LastName" - FROM "Person" AS p1 - WHERE p."FirstName" = p1."FirstName" OR (p."FirstName" IS NULL AND p1."FirstName" IS NULL) + SELECT p0."LastName" + FROM "Person" AS p0 + WHERE p."FirstName" = p0."FirstName" OR (p."FirstName" IS NULL AND p0."FirstName" IS NULL) LIMIT 1) NULLS FIRST """); } @@ -61,29 +61,25 @@ public override async Task Whats_new_2021_sample_6(bool async) AssertSql( """ SELECT ( - SELECT p1."MiddleInitial" - FROM "Person" AS p1 - WHERE p1."Age" = 20 AND p."Id" = p1."Id" + SELECT p0."MiddleInitial" + FROM "Person" AS p0 + WHERE p0."Age" = 20 AND p."Id" = p0."Id" LIMIT 1) FROM "Person" AS p WHERE p."Age" = 20 GROUP BY p."Id" ORDER BY ( - SELECT p1."MiddleInitial" - FROM "Person" AS p1 - WHERE p1."Age" = 20 AND p."Id" = p1."Id" + SELECT p0."MiddleInitial" + FROM "Person" AS p0 + WHERE p0."Age" = 20 AND p."Id" = p0."Id" LIMIT 1) NULLS FIRST """); } - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/27155")] - public override Task Whats_new_2021_sample_10(bool async) - => base.Whats_new_2021_sample_10(async); - private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class Ef6GroupByNpgsqlFixture : Ef6GroupByFixtureBase + public class Ef6GroupByNpgsqlFixture : Ef6GroupByFixtureBase, ITestSqlLoggerFactory { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs index b9987f8e0..012aed426 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs @@ -54,9 +54,9 @@ public override async Task Can_query_entity_which_is_split_selecting_only_part_2 AssertSql( """ -SELECT e."Id", s0."IntValue3", s0."StringValue3" +SELECT e."Id", s."IntValue3", s."StringValue3" FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" """); } @@ -78,14 +78,14 @@ public override async Task Include_reference_to_split_entity(bool async) AssertSql( """ -SELECT e."Id", e."EntityOneId", e."Name", t."Id", t."EntityThreeId", t."IntValue1", t."IntValue2", t."IntValue3", t."IntValue4", t."StringValue1", t."StringValue2", t."StringValue3", t."StringValue4" +SELECT e."Id", e."EntityOneId", e."Name", s1."Id", s1."EntityThreeId", s1."IntValue1", s1."IntValue2", s1."IntValue3", s1."IntValue4", s1."StringValue1", s1."StringValue2", s1."StringValue3", s1."StringValue4" FROM "EntityTwo" AS e LEFT JOIN ( SELECT e0."Id", e0."EntityThreeId", e0."IntValue1", e0."IntValue2", s0."IntValue3", s."IntValue4", e0."StringValue1", e0."StringValue2", s0."StringValue3", s."StringValue4" FROM "EntityOne" AS e0 INNER JOIN "SplitEntityOnePart3" AS s ON e0."Id" = s."Id" INNER JOIN "SplitEntityOnePart2" AS s0 ON e0."Id" = s0."Id" -) AS t ON e."EntityOneId" = t."Id" +) AS s1 ON e."EntityOneId" = s1."Id" """); } @@ -95,14 +95,14 @@ public override async Task Include_collection_to_split_entity(bool async) AssertSql( """ -SELECT e."Id", e."Name", t."Id", t."EntityThreeId", t."IntValue1", t."IntValue2", t."IntValue3", t."IntValue4", t."StringValue1", t."StringValue2", t."StringValue3", t."StringValue4" +SELECT e."Id", e."Name", s1."Id", s1."EntityThreeId", s1."IntValue1", s1."IntValue2", s1."IntValue3", s1."IntValue4", s1."StringValue1", s1."StringValue2", s1."StringValue3", s1."StringValue4" FROM "EntityThree" AS e LEFT JOIN ( SELECT e0."Id", e0."EntityThreeId", e0."IntValue1", e0."IntValue2", s0."IntValue3", s."IntValue4", e0."StringValue1", e0."StringValue2", s0."StringValue3", s."StringValue4" FROM "EntityOne" AS e0 INNER JOIN "SplitEntityOnePart3" AS s ON e0."Id" = s."Id" INNER JOIN "SplitEntityOnePart2" AS s0 ON e0."Id" = s0."Id" -) AS t ON e."Id" = t."EntityThreeId" +) AS s1 ON e."Id" = s1."EntityThreeId" ORDER BY e."Id" NULLS FIRST """); } @@ -113,15 +113,15 @@ public override async Task Include_reference_to_split_entity_including_reference AssertSql( """ -SELECT e."Id", e."EntityOneId", e."Name", t."Id", t."EntityThreeId", t."IntValue1", t."IntValue2", t."IntValue3", t."IntValue4", t."StringValue1", t."StringValue2", t."StringValue3", t."StringValue4", e1."Id", e1."Name" +SELECT e."Id", e."EntityOneId", e."Name", s1."Id", s1."EntityThreeId", s1."IntValue1", s1."IntValue2", s1."IntValue3", s1."IntValue4", s1."StringValue1", s1."StringValue2", s1."StringValue3", s1."StringValue4", e1."Id", e1."Name" FROM "EntityTwo" AS e LEFT JOIN ( SELECT e0."Id", e0."EntityThreeId", e0."IntValue1", e0."IntValue2", s0."IntValue3", s."IntValue4", e0."StringValue1", e0."StringValue2", s0."StringValue3", s."StringValue4" FROM "EntityOne" AS e0 INNER JOIN "SplitEntityOnePart3" AS s ON e0."Id" = s."Id" INNER JOIN "SplitEntityOnePart2" AS s0 ON e0."Id" = s0."Id" -) AS t ON e."EntityOneId" = t."Id" -LEFT JOIN "EntityThree" AS e1 ON t."EntityThreeId" = e1."Id" +) AS s1 ON e."EntityOneId" = s1."Id" +LEFT JOIN "EntityThree" AS e1 ON s1."EntityThreeId" = e1."Id" """); } @@ -131,7 +131,7 @@ public override async Task Include_collection_to_split_entity_including_collecti AssertSql( """ -SELECT e."Id", e."Name", t."Id", t."EntityThreeId", t."IntValue1", t."IntValue2", t."IntValue3", t."IntValue4", t."StringValue1", t."StringValue2", t."StringValue3", t."StringValue4", t."Id0", t."EntityOneId", t."Name" +SELECT e."Id", e."Name", s1."Id", s1."EntityThreeId", s1."IntValue1", s1."IntValue2", s1."IntValue3", s1."IntValue4", s1."StringValue1", s1."StringValue2", s1."StringValue3", s1."StringValue4", s1."Id0", s1."EntityOneId", s1."Name" FROM "EntityThree" AS e LEFT JOIN ( SELECT e0."Id", e0."EntityThreeId", e0."IntValue1", e0."IntValue2", s0."IntValue3", s."IntValue4", e0."StringValue1", e0."StringValue2", s0."StringValue3", s."StringValue4", e1."Id" AS "Id0", e1."EntityOneId", e1."Name" @@ -139,8 +139,8 @@ LEFT JOIN ( INNER JOIN "SplitEntityOnePart3" AS s ON e0."Id" = s."Id" INNER JOIN "SplitEntityOnePart2" AS s0 ON e0."Id" = s0."Id" LEFT JOIN "EntityTwo" AS e1 ON e0."Id" = e1."EntityOneId" -) AS t ON e."Id" = t."EntityThreeId" -ORDER BY e."Id" NULLS FIRST, t."Id" NULLS FIRST +) AS s1 ON e."Id" = s1."EntityThreeId" +ORDER BY e."Id" NULLS FIRST, s1."Id" NULLS FIRST """); } @@ -179,9 +179,9 @@ public override async Task Custom_projection_trim_when_multiple_tables(bool asyn AssertSql( """ -SELECT e."IntValue1", s0."IntValue3", e0."Id", e0."Name" +SELECT e."IntValue1", s."IntValue3", e0."Id", e0."Name" FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" LEFT JOIN "EntityThree" AS e0 ON e."EntityThreeId" = e0."Id" """); } @@ -447,9 +447,9 @@ public override async Task Tpc_entity_owning_a_split_reference_on_leaf_with_tabl AssertSql( """ -SELECT t."Id", t."BaseValue", t."MiddleValue", t."SiblingValue", t."LeafValue", t."Discriminator", l."Id", l."OwnedReference_Id", l."OwnedReference_OwnedIntValue1", l."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", l."OwnedReference_OwnedStringValue1", l."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +SELECT u."Id", u."BaseValue", u."MiddleValue", u."SiblingValue", u."LeafValue", u."Discriminator", l0."Id", l0."OwnedReference_Id", l0."OwnedReference_OwnedIntValue1", l0."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", l0."OwnedReference_OwnedStringValue1", l0."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" FROM ( - SELECT b."Id", b."BaseValue", NULL::int AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" + SELECT b."Id", b."BaseValue", NULL AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" FROM "BaseEntity" AS b UNION ALL SELECT m."Id", m."BaseValue", m."MiddleValue", NULL AS "SiblingValue", NULL AS "LeafValue", 'MiddleEntity' AS "Discriminator" @@ -458,12 +458,12 @@ UNION ALL SELECT s."Id", s."BaseValue", NULL AS "MiddleValue", s."SiblingValue", NULL AS "LeafValue", 'SiblingEntity' AS "Discriminator" FROM "SiblingEntity" AS s UNION ALL - SELECT l0."Id", l0."BaseValue", l0."MiddleValue", NULL AS "SiblingValue", l0."LeafValue", 'LeafEntity' AS "Discriminator" - FROM "LeafEntity" AS l0 -) AS t -LEFT JOIN "LeafEntity" AS l ON t."Id" = l."Id" -LEFT JOIN "OwnedReferencePart4" AS o ON l."Id" = o."LeafEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON l."Id" = o0."LeafEntityId" + SELECT l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" + FROM "LeafEntity" AS l +) AS u +LEFT JOIN "LeafEntity" AS l0 ON u."Id" = l0."Id" +LEFT JOIN "OwnedReferencePart4" AS o ON l0."Id" = o."LeafEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON l0."Id" = o0."LeafEntityId" """); } @@ -576,9 +576,9 @@ public override async Task Tpc_entity_owning_a_split_reference_on_base_without_t AssertSql( """ -SELECT t."Id", t."BaseValue", t."MiddleValue", t."SiblingValue", t."LeafValue", t."Discriminator", o."BaseEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" +SELECT u."Id", u."BaseValue", u."MiddleValue", u."SiblingValue", u."LeafValue", u."Discriminator", o."BaseEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" FROM ( - SELECT b."Id", b."BaseValue", NULL::int AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" + SELECT b."Id", b."BaseValue", NULL AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" FROM "BaseEntity" AS b UNION ALL SELECT m."Id", m."BaseValue", m."MiddleValue", NULL AS "SiblingValue", NULL AS "LeafValue", 'MiddleEntity' AS "Discriminator" @@ -589,8 +589,8 @@ UNION ALL UNION ALL SELECT l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" FROM "LeafEntity" AS l -) AS t -LEFT JOIN "OwnedReferencePart1" AS o ON t."Id" = o."BaseEntityId" +) AS u +LEFT JOIN "OwnedReferencePart1" AS o ON u."Id" = o."BaseEntityId" LEFT JOIN "OwnedReferencePart4" AS o0 ON o."BaseEntityId" = o0."BaseEntityId" LEFT JOIN "OwnedReferencePart3" AS o1 ON o."BaseEntityId" = o1."BaseEntityId" """); @@ -618,9 +618,9 @@ public override async Task Tpc_entity_owning_a_split_reference_on_middle_without AssertSql( """ -SELECT t."Id", t."BaseValue", t."MiddleValue", t."SiblingValue", t."LeafValue", t."Discriminator", o."MiddleEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" +SELECT u."Id", u."BaseValue", u."MiddleValue", u."SiblingValue", u."LeafValue", u."Discriminator", o."MiddleEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" FROM ( - SELECT b."Id", b."BaseValue", NULL::int AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" + SELECT b."Id", b."BaseValue", NULL AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" FROM "BaseEntity" AS b UNION ALL SELECT m."Id", m."BaseValue", m."MiddleValue", NULL AS "SiblingValue", NULL AS "LeafValue", 'MiddleEntity' AS "Discriminator" @@ -631,8 +631,8 @@ UNION ALL UNION ALL SELECT l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" FROM "LeafEntity" AS l -) AS t -LEFT JOIN "OwnedReferencePart1" AS o ON t."Id" = o."MiddleEntityId" +) AS u +LEFT JOIN "OwnedReferencePart1" AS o ON u."Id" = o."MiddleEntityId" LEFT JOIN "OwnedReferencePart4" AS o0 ON o."MiddleEntityId" = o0."MiddleEntityId" LEFT JOIN "OwnedReferencePart3" AS o1 ON o."MiddleEntityId" = o1."MiddleEntityId" """); @@ -684,9 +684,9 @@ public override async Task Tpc_entity_owning_a_split_collection_on_base(bool asy AssertSql( """ -SELECT t."Id", t."BaseValue", t."MiddleValue", t."SiblingValue", t."LeafValue", t."Discriminator", t0."BaseEntityId", t0."Id", t0."OwnedIntValue1", t0."OwnedIntValue2", t0."OwnedIntValue3", t0."OwnedIntValue4", t0."OwnedStringValue1", t0."OwnedStringValue2", t0."OwnedStringValue3", t0."OwnedStringValue4" +SELECT u."Id", u."BaseValue", u."MiddleValue", u."SiblingValue", u."LeafValue", u."Discriminator", s0."BaseEntityId", s0."Id", s0."OwnedIntValue1", s0."OwnedIntValue2", s0."OwnedIntValue3", s0."OwnedIntValue4", s0."OwnedStringValue1", s0."OwnedStringValue2", s0."OwnedStringValue3", s0."OwnedStringValue4" FROM ( - SELECT b."Id", b."BaseValue", NULL::int AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" + SELECT b."Id", b."BaseValue", NULL AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" FROM "BaseEntity" AS b UNION ALL SELECT m."Id", m."BaseValue", m."MiddleValue", NULL AS "SiblingValue", NULL AS "LeafValue", 'MiddleEntity' AS "Discriminator" @@ -697,14 +697,14 @@ UNION ALL UNION ALL SELECT l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" FROM "LeafEntity" AS l -) AS t +) AS u LEFT JOIN ( SELECT o."BaseEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" FROM "OwnedReferencePart1" AS o INNER JOIN "OwnedReferencePart4" AS o0 ON o."BaseEntityId" = o0."BaseEntityId" AND o."Id" = o0."Id" INNER JOIN "OwnedReferencePart3" AS o1 ON o."BaseEntityId" = o1."BaseEntityId" AND o."Id" = o1."Id" -) AS t0 ON t."Id" = t0."BaseEntityId" -ORDER BY t."Id" NULLS FIRST, t0."BaseEntityId" NULLS FIRST +) AS s0 ON u."Id" = s0."BaseEntityId" +ORDER BY u."Id" NULLS FIRST, s0."BaseEntityId" NULLS FIRST """); } @@ -730,9 +730,9 @@ public override async Task Tpc_entity_owning_a_split_collection_on_middle(bool a AssertSql( """ -SELECT t."Id", t."BaseValue", t."MiddleValue", t."SiblingValue", t."LeafValue", t."Discriminator", t0."MiddleEntityId", t0."Id", t0."OwnedIntValue1", t0."OwnedIntValue2", t0."OwnedIntValue3", t0."OwnedIntValue4", t0."OwnedStringValue1", t0."OwnedStringValue2", t0."OwnedStringValue3", t0."OwnedStringValue4" +SELECT u."Id", u."BaseValue", u."MiddleValue", u."SiblingValue", u."LeafValue", u."Discriminator", s0."MiddleEntityId", s0."Id", s0."OwnedIntValue1", s0."OwnedIntValue2", s0."OwnedIntValue3", s0."OwnedIntValue4", s0."OwnedStringValue1", s0."OwnedStringValue2", s0."OwnedStringValue3", s0."OwnedStringValue4" FROM ( - SELECT b."Id", b."BaseValue", NULL::int AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" + SELECT b."Id", b."BaseValue", NULL AS "MiddleValue", NULL::int AS "SiblingValue", NULL::int AS "LeafValue", 'BaseEntity' AS "Discriminator" FROM "BaseEntity" AS b UNION ALL SELECT m."Id", m."BaseValue", m."MiddleValue", NULL AS "SiblingValue", NULL AS "LeafValue", 'MiddleEntity' AS "Discriminator" @@ -743,14 +743,14 @@ UNION ALL UNION ALL SELECT l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" FROM "LeafEntity" AS l -) AS t +) AS u LEFT JOIN ( SELECT o."MiddleEntityId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o1."OwnedIntValue3", o0."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o1."OwnedStringValue3", o0."OwnedStringValue4" FROM "OwnedReferencePart1" AS o INNER JOIN "OwnedReferencePart4" AS o0 ON o."MiddleEntityId" = o0."MiddleEntityId" AND o."Id" = o0."Id" INNER JOIN "OwnedReferencePart3" AS o1 ON o."MiddleEntityId" = o1."MiddleEntityId" AND o."Id" = o1."Id" -) AS t0 ON t."Id" = t0."MiddleEntityId" -ORDER BY t."Id" NULLS FIRST, t0."MiddleEntityId" NULLS FIRST +) AS s0 ON u."Id" = s0."MiddleEntityId" +ORDER BY u."Id" NULLS FIRST, s0."MiddleEntityId" NULLS FIRST """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index b68ebbb89..7467b28f6 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; @@ -38,7 +39,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = 'sad'::test.mapped_enum """); @@ -56,7 +57,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."SchemaQualifiedEnum" = 'Happy (PgName)'::test.schema_qualified_enum """); @@ -77,7 +78,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -98,7 +99,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -119,7 +120,7 @@ await AssertQuery( """ @__sad_0='1' -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedEnum" = @__sad_0 """); @@ -140,7 +141,7 @@ await AssertQuery( """ @__sad_0='Sad' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum" = @__sad_0 """); @@ -159,7 +160,7 @@ await AssertQuery( AssertSql( """ -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."MappedEnum"::text LIKE '%sa%' """); @@ -180,7 +181,7 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."ByteEnum" = ANY (@__values_0) """); @@ -201,12 +202,33 @@ await AssertQuery( """ @__values_0='0x01' (DbType = Object) -SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum" +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" FROM test."SomeEntities" AS s WHERE s."UnmappedByteEnum" = ANY (@__values_0) """); } + [ConditionalTheory] // #3433 + [MemberData(nameof(IsAsyncData))] + public async Task Where_uppercase_enum_array_contains_enum(bool async) + { + await using var ctx = CreateContext(); + + List values = [UppercaseNamedEnum.Sad]; + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.UppercaseNamedEnum))); + + AssertSql( + """ +@__values_0={ 'Sad' } (DbType = Object) + +SELECT s."Id", s."ByteEnum", s."EnumValue", s."InferredEnum", s."MappedEnum", s."SchemaQualifiedEnum", s."UnmappedByteEnum", s."UnmappedEnum", s."UppercaseNamedEnum" +FROM test."SomeEntities" AS s +WHERE s."UppercaseNamedEnum" = ANY (@__values_0) +"""); + } + #endregion #region Support @@ -217,28 +239,18 @@ protected EnumContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class EnumContext : PoolableDbContext + public class EnumContext(DbContextOptions options) : PoolableDbContext(options) { // ReSharper disable once UnusedAutoPropertyAccessor.Global public DbSet SomeEntities { get; set; } - public EnumContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder builder) - => builder - .HasPostgresEnum("mapped_enum", new[] { "happy", "sad" }) - .HasPostgresEnum() - .HasPostgresEnum() - .HasDefaultSchema("test") - .HasPostgresEnum(); - - public static void Seed(EnumContext context) + => builder.HasDefaultSchema("test"); + + public static async Task SeedAsync(EnumContext context) { context.AddRange(EnumData.CreateSomeEnumEntities()); - context.SaveChanges(); + await context.SaveChangesAsync(); } } @@ -249,6 +261,7 @@ public class SomeEnumEntity public UnmappedEnum UnmappedEnum { get; set; } public InferredEnum InferredEnum { get; set; } public SchemaQualifiedEnum SchemaQualifiedEnum { get; set; } + public UppercaseNamedEnum UppercaseNamedEnum { get; set; } public ByteEnum ByteEnum { get; set; } public UnmappedByteEnum UnmappedByteEnum { get; set; } public int EnumValue { get; set; } @@ -279,6 +292,12 @@ public enum SchemaQualifiedEnum Sad } + public enum UppercaseNamedEnum + { + Happy, + Sad + } + public enum ByteEnum : byte { Happy, @@ -292,31 +311,37 @@ public enum UnmappedByteEnum : byte } // ReSharper disable once ClassNeverInstantiated.Global - public class EnumFixture : SharedStoreFixtureBase, IQueryFixtureBase + public class EnumFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory { 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") + .MapEnum("UpperCaseEnum", "test"); + + return optionsBuilder; } private EnumData _expectedData; - protected override void Seed(EnumContext context) - => EnumContext.Seed(context); + protected override Task SeedAsync(EnumContext context) + => EnumContext.SeedAsync(context); public Func GetContextCreator() => CreateContext; @@ -345,6 +370,7 @@ public IReadOnlyDictionary EntityAsserters Assert.Equal(ee.UnmappedEnum, aa.UnmappedEnum); Assert.Equal(ee.InferredEnum, aa.InferredEnum); Assert.Equal(ee.SchemaQualifiedEnum, aa.SchemaQualifiedEnum); + Assert.Equal(ee.UppercaseNamedEnum, aa.UppercaseNamedEnum); Assert.Equal(ee.ByteEnum, aa.ByteEnum); Assert.Equal(ee.UnmappedByteEnum, aa.UnmappedByteEnum); Assert.Equal(ee.EnumValue, aa.EnumValue); @@ -374,6 +400,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Happy, InferredEnum = InferredEnum.Happy, SchemaQualifiedEnum = SchemaQualifiedEnum.Happy, + UppercaseNamedEnum = UppercaseNamedEnum.Happy, ByteEnum = ByteEnum.Happy, UnmappedByteEnum = UnmappedByteEnum.Happy, EnumValue = (int)MappedEnum.Happy @@ -385,6 +412,7 @@ public static IReadOnlyList CreateSomeEnumEntities() UnmappedEnum = UnmappedEnum.Sad, InferredEnum = InferredEnum.Sad, SchemaQualifiedEnum = SchemaQualifiedEnum.Sad, + UppercaseNamedEnum = UppercaseNamedEnum.Sad, ByteEnum = ByteEnum.Sad, UnmappedByteEnum = UnmappedByteEnum.Sad, EnumValue = (int)MappedEnum.Sad diff --git a/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs index 76b581831..9e75660b0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class FieldsOnlyLoadNpgsqlTest : FieldsOnlyLoadTestBase +public class FieldsOnlyLoadNpgsqlTest(FieldsOnlyLoadNpgsqlTest.FieldsOnlyLoadNpgsqlFixture fixture) + : FieldsOnlyLoadTestBase(fixture) { - public FieldsOnlyLoadNpgsqlTest(FieldsOnlyLoadNpgsqlFixture fixture) - : base(fixture) - { - } - public class FieldsOnlyLoadNpgsqlFixture : FieldsOnlyLoadFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs index c94935591..dc391c993 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs @@ -1,15 +1,12 @@ using System.Data.Common; using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Xunit.Sdk; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class FromSqlQueryNpgsqlTest : FromSqlQueryTestBase> +public class FromSqlQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) + : FromSqlQueryTestBase>(fixture) { - public FromSqlQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) - : base(fixture) - { - } - [ConditionalTheory(Skip = "https://github.com/aspnet/EntityFramework/issues/{6563,20364}")] public override Task Bad_data_error_handling_invalid_cast(bool async) => base.Bad_data_error_handling_invalid_cast(async); @@ -18,8 +15,12 @@ public override Task Bad_data_error_handling_invalid_cast(bool async) public override Task Bad_data_error_handling_invalid_cast_projection(bool async) => base.Bad_data_error_handling_invalid_cast_projection(async); - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] + // The test attempts to project out a column with the wrong case; this works on other databases, and fails when EF tries to materialize. + // But in PG this fails at the database since PG is case-sensitive and the column does not exist. + public override Task FromSqlRaw_queryable_simple_different_cased_columns_and_not_enough_columns_throws(bool async) + => Assert.ThrowsAsync( + () => base.FromSqlRaw_queryable_simple_different_cased_columns_and_not_enough_columns_throws(async)); + public override async Task FromSqlInterpolated_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated( bool async) { diff --git a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs index 8c843a02d..fb7483f05 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs @@ -35,7 +35,10 @@ await AssertQuery( ss => ss.Set().Where(c => c.FirstName != null && c.FirstName.StartsWith(param))); } - public class FunkyDataQueryNpgsqlFixture : FunkyDataQueryFixtureBase + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class FunkyDataQueryNpgsqlFixture : FunkyDataQueryFixtureBase, ITestSqlLoggerFactory { private FunkyDataData _expectedData; @@ -74,10 +77,10 @@ public override ISetSource GetExpectedData() return _expectedData; } - protected override void Seed(FunkyDataContext context) + protected override async Task SeedAsync(FunkyDataContext context) { context.FunkyCustomers.AddRange(GetExpectedData().Set()); - context.SaveChanges(); + await context.SaveChangesAsync(); } } } diff --git a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs index 7648c8287..fd76749d3 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs @@ -31,7 +31,7 @@ public void FuzzyStringMatchSoundex() .Select(x => EF.Functions.FuzzyStringMatchSoundex(x.Text)) .ToArray(); - AssertContainsSql(@"soundex(f.""Text"")"); + AssertContainsSql("""soundex(f."Text")"""); } [Fact] @@ -42,7 +42,7 @@ public void FuzzyStringMatchDifference() .Select(x => EF.Functions.FuzzyStringMatchDifference(x.Text, "target")) .ToArray(); - AssertContainsSql(@"difference(f.""Text"", 'target')"); + AssertContainsSql("""difference(f."Text", 'target')"""); } [Fact] @@ -53,7 +53,7 @@ public void FuzzyStringMatchLevenshtein() .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target")) .ToArray(); - AssertContainsSql(@"levenshtein(f.""Text"", 'target')"); + AssertContainsSql("""levenshtein(f."Text", 'target')"""); } [Fact] @@ -64,7 +64,7 @@ public void FuzzyStringMatchLevenshtein_With_Costs() .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target", 1, 2, 3)) .ToArray(); - AssertContainsSql(@"levenshtein(f.""Text"", 'target', 1, 2, 3)"); + AssertContainsSql("""levenshtein(f."Text", 'target', 1, 2, 3)"""); } [Fact] @@ -75,7 +75,7 @@ public void FuzzyStringMatchLevenshteinLessEqual() .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 5)) .ToArray(); - AssertContainsSql(@"levenshtein_less_equal(f.""Text"", 'target', 5)"); + AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 5)"""); } [Fact] @@ -86,7 +86,7 @@ public void FuzzyStringMatchLevenshteinLessEqual_With_Costs() .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 1, 2, 3, 5)) .ToArray(); - AssertContainsSql(@"levenshtein_less_equal(f.""Text"", 'target', 1, 2, 3, 5)"); + AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 1, 2, 3, 5)"""); } [Fact] @@ -97,7 +97,7 @@ public void FuzzyStringMatchMetaphone() .Select(x => EF.Functions.FuzzyStringMatchMetaphone(x.Text, 6)) .ToArray(); - AssertContainsSql(@"metaphone(f.""Text"", 6)"); + AssertContainsSql("""metaphone(f."Text", 6)"""); } [Fact] @@ -108,7 +108,7 @@ public void FuzzyStringMatchDoubleMetaphone() .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphone(x.Text)) .ToArray(); - AssertContainsSql(@"dmetaphone(f.""Text"")"); + AssertContainsSql("""dmetaphone(f."Text")"""); } [Fact] @@ -119,7 +119,7 @@ public void FuzzyStringMatchDoubleMetaphoneAlt() .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphoneAlt(x.Text)) .ToArray(); - AssertContainsSql(@"dmetaphone_alt(f.""Text"")"); + AssertContainsSql("""dmetaphone_alt(f."Text")"""); } #endregion @@ -140,8 +140,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(FuzzyStringMatchContext context) - => FuzzyStringMatchContext.Seed(context); + protected override Task SeedAsync(FuzzyStringMatchContext context) + => FuzzyStringMatchContext.SeedAsync(context); } /// @@ -190,7 +190,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - public static void Seed(FuzzyStringMatchContext context) + public static async Task SeedAsync(FuzzyStringMatchContext context) { for (var i = 1; i <= 9; i++) { @@ -199,7 +199,7 @@ public static void Seed(FuzzyStringMatchContext context) new FuzzyStringMatchTestEntity { Id = i, Text = text }); } - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs index 59e8a92b2..b8a140c27 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs @@ -14,7 +14,9 @@ public override void From_sql_queryable_simple_columns_out_of_order() base.From_sql_queryable_simple_columns_out_of_order(); Assert.Equal( - @"SELECT ""Id"", ""Name"", ""IsAutomatic"", ""AmmunitionType"", ""OwnerFullName"", ""SynergyWithId"" FROM ""Weapons"" ORDER BY ""Name""", + """ +SELECT "Id", "Name", "IsAutomatic", "AmmunitionType", "OwnerFullName", "SynergyWithId" FROM "Weapons" ORDER BY "Name" +""", Sql); } diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs index 81b30ae7d..e2c0054d5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs @@ -49,7 +49,7 @@ public override ISetSource GetExpectedData() return _expectedData; } - protected override void Seed(GearsOfWarContext context) + protected override async Task SeedAsync(GearsOfWarContext context) { // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. @@ -85,10 +85,10 @@ protected override void Seed(GearsOfWarContext context) context.LocustLeaders.AddRange(locustLeaders); context.Factions.AddRange(factions); context.LocustHighCommands.AddRange(locustHighCommands); - context.SaveChanges(); + await context.SaveChangesAsync(); GearsOfWarData.WireUp2(locustLeaders, factions); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs index fa10d5213..684786a6a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs @@ -103,7 +103,7 @@ public override async Task Where_datetimeoffset_utcnow(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Timeline" <> now() """); @@ -133,7 +133,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); @@ -145,7 +145,7 @@ public override async Task Where_datetimeoffset_hour_component(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('hour', m."Timeline" AT TIME ZONE 'UTC')::int = 10 """); @@ -163,7 +163,7 @@ await AssertQuery( """ @__dateTimeOffset_Date_0='0002-03-01T00:00:00.0000000' -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamp >= @__dateTimeOffset_Date_0 """); @@ -188,7 +188,7 @@ await AssertQuery( @__end_1='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) @__dates_2={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE @__start_0 <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @__end_1 AND m."Timeline" = ANY (@__dates_2) """); @@ -297,7 +297,7 @@ public override async Task Where_TimeSpan_Minutes(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE floor(date_part('minute', m."Duration"))::int = 2 """); @@ -309,7 +309,7 @@ public override async Task Where_TimeSpan_Seconds(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE floor(date_part('second', m."Duration"))::int = 3 """); @@ -321,7 +321,7 @@ public override async Task Where_TimeSpan_Milliseconds(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE floor(date_part('millisecond', m."Duration"))::int % 1000 = 456 """); @@ -337,7 +337,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('epoch', m."Duration") / 86400.0 < 0.042000000000000003 """); @@ -353,7 +353,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('epoch', m."Duration") / 3600.0 < 1.02 """); @@ -369,7 +369,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('epoch', m."Duration") / 60.0 < 61.0 """); @@ -385,7 +385,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('epoch', m."Duration") < 3700.0 """); @@ -401,7 +401,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('epoch', m."Duration") / 0.001 < 3700000.0 """); @@ -491,7 +491,7 @@ public override async Task Where_DateOnly_Month(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('month', m."Date")::int = 11 """); @@ -503,7 +503,7 @@ public override async Task Where_DateOnly_Day(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('day', m."Date")::int = 10 """); @@ -515,7 +515,7 @@ public override async Task Where_DateOnly_DayOfYear(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('doy', m."Date")::int = 314 """); @@ -527,7 +527,7 @@ public override async Task Where_DateOnly_DayOfWeek(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE floor(date_part('dow', m."Date"))::int = 6 """); @@ -539,7 +539,7 @@ public override async Task Where_DateOnly_AddYears(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE CAST(m."Date" + INTERVAL '3 years' AS date) = DATE '1993-11-10' """); @@ -551,7 +551,7 @@ public override async Task Where_DateOnly_AddMonths(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE CAST(m."Date" + INTERVAL '3 months' AS date) = DATE '1991-02-10' """); @@ -563,7 +563,7 @@ public override async Task Where_DateOnly_AddDays(bool async) AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Date" + 3 = DATE '1990-11-13' """); @@ -644,7 +644,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('hour', m."Time")::int = 10 """); @@ -658,7 +658,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('minute', m."Time")::int = 15 """); @@ -672,7 +672,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_part('second', m."Time")::int = 50 """); @@ -689,7 +689,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time" + INTERVAL '3 hours' = TIME '13:15:50.5' """); @@ -703,7 +703,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time" + INTERVAL '3 mins' = TIME '10:18:50.5' """); @@ -717,7 +717,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time" + INTERVAL '03:00:00' = TIME '13:15:50.5' """); @@ -731,7 +731,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time" >= TIME '10:00:00' AND m."Time" < TIME '11:00:00' """); @@ -745,7 +745,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time" - TIME '10:00:00' = INTERVAL '00:15:50.5' """); @@ -762,7 +762,7 @@ public virtual async Task TimeOnly_FromTimeSpan() AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Duration"::time without time zone = TIME '01:02:03' LIMIT 2 @@ -779,7 +779,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE m."Time"::interval = INTERVAL '15:30:10' """); diff --git a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs index 2df5c0cb7..6fbb0e034 100644 --- a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs @@ -1,11 +1,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class InheritanceRelationshipsQueryNpgsqlTest : InheritanceRelationshipsQueryTestBase +public class InheritanceRelationshipsQueryNpgsqlTest(InheritanceRelationshipsQueryNpgsqlFixture fixture) + : InheritanceRelationshipsQueryTestBase(fixture) { - public InheritanceRelationshipsQueryNpgsqlTest(InheritanceRelationshipsQueryNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void ClearLog() { } } diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs index a0f3e9980..a46fd3c96 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs @@ -55,8 +55,10 @@ public void Literal_document() ctx.JsonbEntities.Where( e => e.CustomerDocument == JsonDocument.Parse( - @" -{ ""Name"": ""Test customer"", ""Age"": 80 }", default))); + """ + + { "Name": "Test customer", "Age": 80 } + """, default))); AssertSql( """ @@ -428,7 +430,7 @@ LIMIT 2 public void JsonContains_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContains(e.CustomerElement, element)); @@ -450,7 +452,7 @@ public void JsonContains_with_string() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonContains(e.CustomerElement, @"{""Name"": ""Joe"", ""Age"": 25}")); + EF.Functions.JsonContains(e.CustomerElement, """{"Name": "Joe", "Age": 25}""")); Assert.Equal(1, count); AssertSql( @@ -465,7 +467,7 @@ SELECT count(*)::int public void JsonContained_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContained(element, e.CustomerElement)); @@ -487,7 +489,7 @@ public void JsonContained_with_string() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonContained(@"{""Name"": ""Joe"", ""Age"": 25}", e.CustomerElement)); + EF.Functions.JsonContained("""{"Name": "Joe", "Age": 25}""", e.CustomerElement)); // Assert.Equal(1, count); AssertSql( @@ -593,17 +595,12 @@ protected JsonDomQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class JsonDomQueryContext : PoolableDbContext + public class JsonDomQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet JsonbEntities { get; set; } public DbSet JsonEntities { get; set; } - public JsonDomQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(JsonDomQueryContext context) + public static async Task SeedAsync(JsonDomQueryContext context) { var (customer1, customer2, customer3) = (CreateCustomer1(), CreateCustomer2(), CreateCustomer3()); @@ -645,7 +642,8 @@ public static void Seed(JsonDomQueryContext context) CustomerDocument = customer3, CustomerElement = customer3.RootElement }); - context.SaveChanges(); + + await context.SaveChangesAsync(); static JsonDocument CreateCustomer1() => JsonDocument.Parse( @@ -736,7 +734,9 @@ static JsonDocument CreateCustomer2() """); static JsonDocument CreateCustomer3() - => JsonDocument.Parse(@"""foo"""); + => JsonDocument.Parse(""" + "foo" + """); } } @@ -770,8 +770,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(JsonDomQueryContext context) - => JsonDomQueryContext.Seed(context); + protected override Task SeedAsync(JsonDomQueryContext context) + => JsonDomQueryContext.SeedAsync(context); } #endregion diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs index f804c6040..bb108fc6e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs @@ -499,7 +499,7 @@ LIMIT 2 public void JsonContains_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContains(e.Customer, element)); @@ -521,7 +521,7 @@ public void JsonContains_with_string_literal() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonContains(e.Customer, @"{""Name"": ""Joe"", ""Age"": 25}")); + EF.Functions.JsonContains(e.Customer, """{"Name": "Joe", "Age": 25}""")); Assert.Equal(1, count); AssertSql( @@ -536,7 +536,7 @@ SELECT count(*)::int public void JsonContains_with_string_parameter() { using var ctx = CreateContext(); - var someJson = @"{""Name"": ""Joe"", ""Age"": 25}"; + var someJson = """{"Name": "Joe", "Age": 25}"""; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContains(e.Customer, someJson)); @@ -556,7 +556,7 @@ WHERE j."Customer" @> @__someJson_1 public void JsonContained_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContained(element, e.Customer)); @@ -578,7 +578,7 @@ public void JsonContained_with_string_literal() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonContained(@"{""Name"": ""Joe"", ""Age"": 25}", e.Customer)); + EF.Functions.JsonContained("""{"Name": "Joe", "Age": 25}""", e.Customer)); Assert.Equal(1, count); AssertSql( @@ -593,7 +593,7 @@ SELECT count(*)::int public void JsonContained_with_string_parameter() { using var ctx = CreateContext(); - var someJson = @"{""Name"": ""Joe"", ""Age"": 25}"; + var someJson = """{"Name": "Joe", "Age": 25}"""; var count = ctx.JsonbEntities.Count( e => EF.Functions.JsonContained(someJson, e.Customer)); @@ -704,24 +704,19 @@ protected JsonPocoQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class JsonPocoQueryContext : PoolableDbContext + public class JsonPocoQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet JsonbEntities { get; set; } public DbSet JsonEntities { get; set; } - public JsonPocoQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(JsonPocoQueryContext context) + public static async Task SeedAsync(JsonPocoQueryContext context) { context.JsonbEntities.AddRange( new JsonbEntity { Id = 1, Customer = CreateCustomer1(), - ToplevelArray = new[] { "one", "two", "three" } + ToplevelArray = ["one", "two", "three"] }, new JsonbEntity { Id = 2, Customer = CreateCustomer2() }); context.JsonEntities.AddRange( @@ -729,10 +724,11 @@ public static void Seed(JsonPocoQueryContext context) { Id = 1, Customer = CreateCustomer1(), - ToplevelArray = new[] { "one", "two", "three" } + ToplevelArray = ["one", "two", "three"] }, new JsonEntity { Id = 2, Customer = CreateCustomer2() }); - context.SaveChanges(); + + await context.SaveChangesAsync(); static Customer CreateCustomer1() => new() @@ -750,12 +746,12 @@ static Customer CreateCustomer1() SomeProperty = 10, SomeNullableInt = 20, SomeNullableGuid = Guid.Parse("d5f2685d-e5c4-47e5-97aa-d0266154eb2d"), - IntArray = new[] { 3, 4 }, - IntList = new List { 3, 4 } + IntArray = [3, 4], + IntList = [3, 4] } }, - Orders = new[] - { + Orders = + [ new Order { Price = 99.5m, ShippingAddress = "Some address 1", @@ -764,7 +760,7 @@ static Customer CreateCustomer1() { Price = 23, ShippingAddress = "Some address 2", } - }, + ], VariousTypes = new VariousTypes { String = "foo", @@ -794,22 +790,22 @@ static Customer CreateCustomer2() SomeProperty = 20, SomeNullableInt = null, SomeNullableGuid = null, - IntArray = new[] { 5, 6, 7 }, - IntList = new List - { + IntArray = [5, 6, 7], + IntList = + [ 5, 6, 7 - } + ] } }, - Orders = new[] - { + Orders = + [ new Order { Price = 5, ShippingAddress = "Moe's address", } - }, + ], VariousTypes = new VariousTypes { String = "bar", @@ -866,8 +862,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(JsonPocoQueryContext context) - => JsonPocoQueryContext.Seed(context); + protected override Task SeedAsync(JsonPocoQueryContext context) + => JsonPocoQueryContext.SeedAsync(context); } public class Customer diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs deleted file mode 100644 index 5a9004d92..000000000 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs +++ /dev/null @@ -1,469 +0,0 @@ -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; - -public class JsonQueryAdHocNpgsqlTest : JsonQueryAdHocTestBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/32235")] - public override Task Junk_in_json_basic_tracking(bool async) - => base.Junk_in_json_basic_tracking(async); - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/32235")] - public override Task Junk_in_json_basic_no_tracking(bool async) - => base.Junk_in_json_basic_no_tracking(async); - - [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual async Task Json_predicate_on_bytea(bool async) - { - var contextFactory = await InitializeAsync( - seed: context => - { - context.Entities.AddRange( - new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = new byte[] { 1, 2, 3 } } }, - new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = new byte[] { 1, 2, 4 } } }); - context.SaveChanges(); - }); - - using (var context = contextFactory.CreateContext()) - { - var query = context.Entities.Where(x => x.JsonEntity.Bytea == new byte[] { 1, 2, 4 }); - - var result = async - ? await query.SingleAsync() - : query.Single(); - - Assert.Equal(2, result.Id); - - AssertSql( - """ -SELECT e."Id", e."JsonEntity" -FROM "Entities" AS e -WHERE (decode(e."JsonEntity" ->> 'Bytea', 'base64')) = BYTEA E'\\x010204' -LIMIT 2 -"""); - } - } - - [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual async Task Json_predicate_on_interval(bool async) - { - var contextFactory = await InitializeAsync( - seed: context => - { - context.Entities.AddRange( - new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(1, 2, 3, 4, 123, 456) } }, - new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(2, 2, 3, 4, 123, 456) } }); - context.SaveChanges(); - }); - - using (var context = contextFactory.CreateContext()) - { - var query = context.Entities.Where(x => x.JsonEntity.Interval == new TimeSpan(2, 2, 3, 4, 123, 456)); - - var result = async - ? await query.SingleAsync() - : query.Single(); - - Assert.Equal(2, result.Id); - - AssertSql( - """ -SELECT e."Id", e."JsonEntity" -FROM "Entities" AS e -WHERE (CAST(e."JsonEntity" ->> 'Interval' AS interval)) = INTERVAL '2 02:03:04.123456' -LIMIT 2 -"""); - } - } - - protected class TypesDbContext : DbContext - { - public TypesDbContext(DbContextOptions options) - : base(options) - { - } - - public DbSet Entities { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity().OwnsOne(b => b.JsonEntity).ToJson(); - } - - public class TypesContainerEntity - { - public int Id { get; set; } - public TypesJsonEntity JsonEntity { get; set; } - } - - public class TypesJsonEntity - { - public byte[] Bytea { get; set; } - public TimeSpan Interval { get; set; } - } - - protected override void Seed29219(MyContext29219 ctx) - { - var entity1 = new MyEntity29219 - { - Id = 1, - Reference = new MyJsonEntity29219 { NonNullableScalar = 10, NullableScalar = 11 }, - Collection = new List - { - new() { NonNullableScalar = 100, NullableScalar = 101 }, - new() { NonNullableScalar = 200, NullableScalar = 201 }, - new() { NonNullableScalar = 300, NullableScalar = null }, - } - }; - - var entity2 = new MyEntity29219 - { - Id = 2, - Reference = new MyJsonEntity29219 { NonNullableScalar = 20, NullableScalar = null }, - Collection = new List { new() { NonNullableScalar = 1001, NullableScalar = null }, } - }; - - ctx.Entities.AddRange(entity1, entity2); - ctx.SaveChanges(); - - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Id", "Reference", "Collection") -VALUES(3, '{{ "NonNullableScalar" : 30 }}', '[{{ "NonNullableScalar" : 10001 }}]') -"""); - } - - protected override void Seed30028(MyContext30028 ctx) - { - // complete - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Id", "Json") -VALUES( -1, -'{{"RootName":"e1","Collection":[{{"BranchName":"e1 c1","Nested":{{"LeafName":"e1 c1 l"}}}},{{"BranchName":"e1 c2","Nested":{{"LeafName":"e1 c2 l"}}}}],"OptionalReference":{{"BranchName":"e1 or","Nested":{{"LeafName":"e1 or l"}}}},"RequiredReference":{{"BranchName":"e1 rr","Nested":{{"LeafName":"e1 rr l"}}}}}}') -"""); - - // missing collection - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Id", "Json") -VALUES( -2, -'{{"RootName":"e2","OptionalReference":{{"BranchName":"e2 or","Nested":{{"LeafName":"e2 or l"}}}},"RequiredReference":{{"BranchName":"e2 rr","Nested":{{"LeafName":"e2 rr l"}}}}}}') -"""); - - // missing optional reference - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Id", "Json") -VALUES( -3, -'{{"RootName":"e3","Collection":[{{"BranchName":"e3 c1","Nested":{{"LeafName":"e3 c1 l"}}}},{{"BranchName":"e3 c2","Nested":{{"LeafName":"e3 c2 l"}}}}],"RequiredReference":{{"BranchName":"e3 rr","Nested":{{"LeafName":"e3 rr l"}}}}}}') -"""); - - // missing required reference - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Id", "Json") -VALUES( -4, -'{{"RootName":"e4","Collection":[{{"BranchName":"e4 c1","Nested":{{"LeafName":"e4 c1 l"}}}},{{"BranchName":"e4 c2","Nested":{{"LeafName":"e4 c2 l"}}}}],"OptionalReference":{{"BranchName":"e4 or","Nested":{{"LeafName":"e4 or l"}}}}}}') -"""); - } - - protected override void SeedArrayOfPrimitives(MyContextArrayOfPrimitives ctx) - { - var entity1 = new MyEntityArrayOfPrimitives - { - Id = 1, - Reference = new MyJsonEntityArrayOfPrimitives - { - IntArray = new[] { 1, 2, 3 }, - ListOfString = new List - { - "Foo", - "Bar", - "Baz" - } - }, - Collection = new List - { - new() { IntArray = new[] { 111, 112, 113 }, ListOfString = new List { "Foo11", "Bar11" } }, - new() { IntArray = new[] { 211, 212, 213 }, ListOfString = new List { "Foo12", "Bar12" } }, - } - }; - - var entity2 = new MyEntityArrayOfPrimitives - { - Id = 2, - Reference = new MyJsonEntityArrayOfPrimitives - { - IntArray = new[] { 10, 20, 30 }, - ListOfString = new List - { - "A", - "B", - "C" - } - }, - Collection = new List - { - new() { IntArray = new[] { 110, 120, 130 }, ListOfString = new List { "A1", "Z1" } }, - new() { IntArray = new[] { 210, 220, 230 }, ListOfString = new List { "A2", "Z2" } }, - } - }; - - ctx.Entities.AddRange(entity1, entity2); - ctx.SaveChanges(); - } - - protected override void SeedJunkInJson(MyContextJunkInJson ctx) - => ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id") -VALUES( -'[{{"JunkReference":{{"Something":"SomeValue" }},"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{{"Foo":"junk value"}}],"NestedCollection":[{{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"}},{{"DoB":"2002-04-02T00:00:00","DummyReference":{{"Foo":5}}}}],"NestedReference":{{"DoB":"2002-03-01T00:00:00"}}}},{{"Name":"c12","Number":12.5,"NestedCollection":[{{"DoB":"2002-06-01T00:00:00"}},{{"DoB":"2002-06-02T00:00:00"}}],"NestedDummy":59,"NestedReference":{{"DoB":"2002-05-01T00:00:00"}}}}]', -'[{{"MyBool":true,"Name":"c11 ctor","JunkReference":{{"Something":"SomeValue","JunkCollection":[{{"Foo":"junk value"}}]}},"NestedCollection":[{{"DoB":"2002-08-01T00:00:00"}},{{"DoB":"2002-08-02T00:00:00"}}],"NestedReference":{{"DoB":"2002-07-01T00:00:00"}}}},{{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{{"DoB":"2002-10-01T00:00:00"}},{{"DoB":"2002-10-02T00:00:00"}}],"JunkCollection":[{{"Foo":"junk value"}}],"NestedReference":{{"DoB":"2002-09-01T00:00:00"}}}}]', -'{{"Name":"r1","JunkCollection":[{{"Foo":"junk value"}}],"JunkReference":{{"Something":"SomeValue" }},"Number":1.5,"NestedCollection":[{{"DoB":"2000-02-01T00:00:00","JunkReference":{{"Something":"SomeValue"}}}},{{"DoB":"2000-02-02T00:00:00"}}],"NestedReference":{{"DoB":"2000-01-01T00:00:00"}}}}', -'{{"MyBool":true,"JunkCollection":[{{"Foo":"junk value"}}],"Name":"r1 ctor","JunkReference":{{"Something":"SomeValue" }},"NestedCollection":[{{"DoB":"2001-02-01T00:00:00"}},{{"DoB":"2001-02-02T00:00:00"}}],"NestedReference":{{"JunkCollection":[{{"Foo":"junk value"}}],"DoB":"2001-01-01T00:00:00"}}}}', -1) -"""); - - protected override void SeedShadowProperties(MyContextShadowProperties ctx) - => ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id", "Name") -VALUES( -'[{{"Name":"e1_c1","ShadowDouble":5.5}},{{"ShadowDouble":20.5,"Name":"e1_c2"}}]', -'[{{"Name":"e1_c1 ctor","ShadowNullableByte":6}},{{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}}]', -'{{"Name":"e1_r", "ShadowString":"Foo"}}', -'{{"ShadowInt":143,"Name":"e1_r ctor"}}', -1, -'e1') -"""); - - protected override void SeedNotICollection(MyContextNotICollection ctx) - { - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Json", "Id") -VALUES( -'{{"Collection":[{{"Bar":11,"Foo":"c11"}},{{"Bar":12,"Foo":"c12"}},{{"Bar":13,"Foo":"c13"}}]}}', -1) -"""); - - ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Json", "Id") -VALUES( -'{{"Collection":[{{"Bar":21,"Foo":"c21"}},{{"Bar":22,"Foo":"c22"}}]}}', -2) -"""); - } - - #region EnumLegacyValues - - [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual async Task Read_enum_property_with_legacy_values(bool async) - { - var contextFactory = await InitializeAsync( - seed: SeedEnumLegacyValues); - - using (var context = contextFactory.CreateContext()) - { - var query = context.Entities.Select( - x => new - { - x.Reference.IntEnum, - x.Reference.ByteEnum, - x.Reference.LongEnum, - x.Reference.NullableEnum - }); - - var exception = async - ? await (Assert.ThrowsAsync(() => query.ToListAsync())) - : Assert.Throws(() => query.ToList()); - - // Conversion failed when converting the text value '...' to data type int - Assert.Equal("22P02", exception.SqlState); - } - } - - [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual async Task Read_json_entity_with_enum_properties_with_legacy_values(bool async) - { - var contextFactory = await InitializeAsync( - seed: SeedEnumLegacyValues, - shouldLogCategory: c => c == DbLoggerCategory.Query.Name); - - using (var context = contextFactory.CreateContext()) - { - var query = context.Entities.Select(x => x.Reference).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Single(result); - Assert.Equal(ByteEnumLegacyValues.Redmond, result[0].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Foo, result[0].IntEnum); - Assert.Equal(LongEnumLegacyValues.Three, result[0].LongEnum); - Assert.Equal(ULongEnumLegacyValues.Three, result[0].ULongEnum); - Assert.Equal(IntEnumLegacyValues.Bar, result[0].NullableEnum); - } - - var testLogger = new TestLogger(); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues)))); - } - - [ConditionalTheory, MemberData(nameof(IsAsyncData))] - public virtual async Task Read_json_entity_collection_with_enum_properties_with_legacy_values(bool async) - { - var contextFactory = await InitializeAsync( - seed: SeedEnumLegacyValues, - shouldLogCategory: c => c == DbLoggerCategory.Query.Name); - - using (var context = contextFactory.CreateContext()) - { - var query = context.Entities.Select(x => x.Collection).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Single(result); - Assert.Equal(2, result[0].Count); - Assert.Equal(ByteEnumLegacyValues.Bellevue, result[0][0].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Foo, result[0][0].IntEnum); - Assert.Equal(LongEnumLegacyValues.One, result[0][0].LongEnum); - Assert.Equal(ULongEnumLegacyValues.One, result[0][0].ULongEnum); - Assert.Equal(IntEnumLegacyValues.Bar, result[0][0].NullableEnum); - Assert.Equal(ByteEnumLegacyValues.Seattle, result[0][1].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Baz, result[0][1].IntEnum); - Assert.Equal(LongEnumLegacyValues.Two, result[0][1].LongEnum); - Assert.Equal(ULongEnumLegacyValues.Two, result[0][1].ULongEnum); - Assert.Null(result[0][1].NullableEnum); - } - - var testLogger = new TestLogger(); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues)))); - Assert.Single( - ListLoggerFactory.Log.Where( - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues)))); - } - - private void SeedEnumLegacyValues(MyContextEnumLegacyValues ctx) - => ctx.Database.ExecuteSqlRaw( - """ -INSERT INTO "Entities" ("Collection", "Reference", "Id", "Name") -VALUES( -'[{{"ByteEnum":"Bellevue","IntEnum":"Foo","LongEnum":"One","ULongEnum":"One","Name":"e1_c1","NullableEnum":"Bar"}},{{"ByteEnum":"Seattle","IntEnum":"Baz","LongEnum":"Two","ULongEnum":"Two","Name":"e1_c2","NullableEnum":null}}]', -'{{"ByteEnum":"Redmond","IntEnum":"Foo","LongEnum":"Three","ULongEnum":"Three","Name":"e1_r","NullableEnum":"Bar"}}', -1, -'e1') -"""); - - private class MyContextEnumLegacyValues : DbContext - { - public MyContextEnumLegacyValues(DbContextOptions options) - : base((new DbContextOptionsBuilder(options)).ConfigureWarnings(b => b.Log(CoreEventId.StringEnumValueInJson)).Options) - { - } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public DbSet Entities { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Reference, b => b.ToJson()); - modelBuilder.Entity().OwnsMany(x => x.Collection, b => b.ToJson()); - } - } - - private class MyEntityEnumLegacyValues - { - public int Id { get; set; } - public string Name { get; set; } - - public MyJsonEntityEnumLegacyValues Reference { get; set; } - public List Collection { get; set; } - } - - private class MyJsonEntityEnumLegacyValues - { - public string Name { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public IntEnumLegacyValues IntEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public ByteEnumLegacyValues ByteEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public LongEnumLegacyValues LongEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public ULongEnumLegacyValues ULongEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public IntEnumLegacyValues? NullableEnum { get; set; } - } - - private enum IntEnumLegacyValues - { - Foo = int.MinValue, - Bar, - Baz = int.MaxValue, - } - - private enum ByteEnumLegacyValues : byte - { - Seattle, - Redmond, - Bellevue = 255, - } - - private enum LongEnumLegacyValues : long - { - One = long.MinValue, - Two = 1, - Three = long.MaxValue, - } - - private enum ULongEnumLegacyValues : ulong - { - One = ulong.MinValue, - Two = 1, - Three = ulong.MaxValue, - } - - #endregion - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs index 081b7c140..d1e3305b6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs @@ -3,7 +3,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class JsonQueryNpgsqlTest : JsonQueryTestBase +public class JsonQueryNpgsqlTest : JsonQueryRelationalTestBase { public JsonQueryNpgsqlTest(JsonQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) @@ -34,6 +34,17 @@ public override async Task Basic_json_projection_owner_entity_NoTracking(bool as """); } + public override async Task Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Basic_json_projection_owner_entity_duplicated(bool async) { await base.Basic_json_projection_owner_entity_duplicated(async); @@ -56,6 +67,17 @@ public override async Task Basic_json_projection_owner_entity_duplicated_NoTrack """); } + public override async Task Basic_json_projection_owner_entity_duplicated_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_duplicated_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."Name", j."OwnedCollection", j."OwnedCollection" +FROM "JsonEntitiesSingleOwned" AS j +"""); + } + public override async Task Basic_json_projection_owner_entity_twice(bool async) { await base.Basic_json_projection_owner_entity_twice(async); @@ -78,6 +100,17 @@ public override async Task Basic_json_projection_owner_entity_twice_NoTracking(b """); } + public override async Task Basic_json_projection_owner_entity_twice_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_twice_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Project_json_reference_in_tracking_query_fails(bool async) { await base.Project_json_reference_in_tracking_query_fails(async); @@ -113,6 +146,17 @@ public override async Task Basic_json_projection_owned_reference_root(bool async """); } + public override async Task Basic_json_projection_owned_reference_root_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_root_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot", j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Basic_json_projection_owned_reference_duplicated2(bool async) { await base.Basic_json_projection_owned_reference_duplicated2(async); @@ -125,6 +169,18 @@ ORDER BY j."Id" NULLS FIRST """); } + public override async Task Basic_json_projection_owned_reference_duplicated2_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_duplicated2_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot", j."Id", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}' +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + public override async Task Basic_json_projection_owned_reference_duplicated(bool async) { await base.Basic_json_projection_owned_reference_duplicated(async); @@ -137,6 +193,18 @@ ORDER BY j."Id" NULLS FIRST """); } + public override async Task Basic_json_projection_owned_reference_duplicated_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_duplicated_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot", j."Id", j."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j."OwnedReferenceRoot", j."OwnedReferenceRoot" -> 'OwnedReferenceBranch' +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + public override async Task Basic_json_projection_owned_collection_root(bool async) { await base.Basic_json_projection_owned_collection_root(async); @@ -148,6 +216,17 @@ public override async Task Basic_json_projection_owned_collection_root(bool asyn """); } + public override async Task Basic_json_projection_owned_collection_root_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_collection_root_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot", j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Basic_json_projection_owned_reference_branch(bool async) { await base.Basic_json_projection_owned_reference_branch(async); @@ -159,6 +238,17 @@ public override async Task Basic_json_projection_owned_reference_branch(bool asy """); } + public override async Task Basic_json_projection_owned_reference_branch_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_branch_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Basic_json_projection_owned_collection_branch(bool async) { await base.Basic_json_projection_owned_collection_branch(async); @@ -170,6 +260,17 @@ public override async Task Basic_json_projection_owned_collection_branch(bool as """); } + public override async Task Basic_json_projection_owned_collection_branch_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_collection_branch_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" -> 'OwnedCollectionBranch', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + public override async Task Basic_json_projection_owned_reference_leaf(bool async) { await base.Basic_json_projection_owned_reference_leaf(async); @@ -232,7 +333,7 @@ public override async Task Json_projection_enum_with_custom_conversion(bool asyn AssertSql( """ -SELECT j."Id", CAST(j.json_reference_custom_naming ->> 'CustomEnum' AS integer) AS "Enum" +SELECT j."Id", CAST(j.json_reference_custom_naming ->> '1CustomEnum' AS integer) AS "Enum" FROM "JsonEntitiesCustomNaming" AS j """); } @@ -279,16 +380,16 @@ public override async Task Json_subquery_property_pushdown_length(bool async) """ @__p_0='3' -SELECT length(t0.c)::int +SELECT length(j1.c)::int FROM ( - SELECT DISTINCT t.c + SELECT DISTINCT j0.c FROM ( SELECT j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}' AS c FROM "JsonEntitiesBasic" AS j ORDER BY j."Id" NULLS FIRST LIMIT @__p_0 - ) AS t -) AS t0 + ) AS j0 +) AS j1 """); } @@ -300,16 +401,16 @@ public override async Task Json_subquery_reference_pushdown_reference(bool async """ @__p_0='10' -SELECT t0.c -> 'OwnedReferenceBranch', t0."Id" +SELECT j1.c -> 'OwnedReferenceBranch', j1."Id" FROM ( - SELECT DISTINCT t.c AS c, t."Id" + SELECT DISTINCT j0.c AS c, j0."Id" FROM ( SELECT j."OwnedReferenceRoot" AS c, j."Id" FROM "JsonEntitiesBasic" AS j ORDER BY j."Id" NULLS FIRST LIMIT @__p_0 - ) AS t -) AS t0 + ) AS j0 +) AS j1 """); } @@ -368,24 +469,24 @@ public override async Task Json_subquery_reference_pushdown_reference_pushdown_r """ @__p_0='10' -SELECT t2.c -> 'OwnedReferenceLeaf', t2."Id" +SELECT j3.c -> 'OwnedReferenceLeaf', j3."Id" FROM ( - SELECT DISTINCT t1.c AS c, t1."Id" + SELECT DISTINCT j2.c AS c, j2."Id" FROM ( - SELECT t0.c -> 'OwnedReferenceBranch' AS c, t0."Id" + SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" FROM ( - SELECT DISTINCT t.c AS c, t."Id", t.c AS c0 + SELECT DISTINCT j0.c AS c, j0."Id", j0.c AS c0 FROM ( SELECT j."OwnedReferenceRoot" AS c, j."Id" FROM "JsonEntitiesBasic" AS j ORDER BY j."Id" NULLS FIRST LIMIT @__p_0 - ) AS t - ) AS t0 - ORDER BY t0.c0 ->> 'Name' NULLS FIRST + ) AS j0 + ) AS j1 + ORDER BY j1.c0 ->> 'Name' NULLS FIRST LIMIT @__p_0 - ) AS t1 -) AS t2 + ) AS j2 +) AS j3 """); } @@ -397,24 +498,24 @@ public override async Task Json_subquery_reference_pushdown_reference_pushdown_c """ @__p_0='10' -SELECT t2.c -> 'OwnedCollectionLeaf', t2."Id" +SELECT j3.c -> 'OwnedCollectionLeaf', j3."Id" FROM ( - SELECT DISTINCT t1.c AS c, t1."Id" + SELECT DISTINCT j2.c AS c, j2."Id" FROM ( - SELECT t0.c -> 'OwnedReferenceBranch' AS c, t0."Id" + SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" FROM ( - SELECT DISTINCT t.c AS c, t."Id", t.c AS c0 + SELECT DISTINCT j0.c AS c, j0."Id", j0.c AS c0 FROM ( SELECT j."OwnedReferenceRoot" AS c, j."Id" FROM "JsonEntitiesBasic" AS j ORDER BY j."Id" NULLS FIRST LIMIT @__p_0 - ) AS t - ) AS t0 - ORDER BY t0.c0 ->> 'Name' NULLS FIRST + ) AS j0 + ) AS j1 + ORDER BY j1.c0 ->> 'Name' NULLS FIRST LIMIT @__p_0 - ) AS t1 -) AS t2 + ) AS j2 +) AS j3 """); } @@ -426,16 +527,16 @@ public override async Task Json_subquery_reference_pushdown_property(bool async) """ @__p_0='10' -SELECT t0.c ->> 'SomethingSomething' +SELECT j1.c ->> 'SomethingSomething' FROM ( - SELECT DISTINCT t.c AS c, t."Id" + SELECT DISTINCT j0.c AS c, j0."Id" FROM ( SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}' AS c, j."Id" FROM "JsonEntitiesBasic" AS j ORDER BY j."Id" NULLS FIRST LIMIT @__p_0 - ) AS t -) AS t0 + ) AS j0 +) AS j1 """); } @@ -456,7 +557,7 @@ public override async Task Custom_naming_projection_owned_reference(bool async) AssertSql( """ -SELECT j.json_reference_custom_naming -> 'CustomOwnedReferenceBranch', j."Id" +SELECT j.json_reference_custom_naming -> 'Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸', j."Id" FROM "JsonEntitiesCustomNaming" AS j """); } @@ -479,7 +580,7 @@ public override async Task Custom_naming_projection_owned_scalar(bool async) AssertSql( """ -SELECT CAST(j.json_reference_custom_naming #>> '{CustomOwnedReferenceBranch,CustomFraction}' AS double precision) +SELECT CAST(j.json_reference_custom_naming #>> ARRAY['Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸','ユニコーンFraction一角獣']::text[] AS double precision) FROM "JsonEntitiesCustomNaming" AS j """); } @@ -490,7 +591,7 @@ public override async Task Custom_naming_projection_everything(bool async) AssertSql( """ -SELECT j."Id", j."Title", j.json_collection_custom_naming, j.json_reference_custom_naming, j.json_reference_custom_naming, j.json_reference_custom_naming -> 'CustomOwnedReferenceBranch', j.json_collection_custom_naming, j.json_reference_custom_naming -> 'CustomOwnedCollectionBranch', j.json_reference_custom_naming ->> 'CustomName', CAST(j.json_reference_custom_naming #>> '{CustomOwnedReferenceBranch,CustomFraction}' AS double precision) +SELECT j."Id", j."Title", j.json_collection_custom_naming, j.json_reference_custom_naming, j.json_reference_custom_naming, j.json_reference_custom_naming -> 'Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸', j.json_collection_custom_naming, j.json_reference_custom_naming -> 'CustomOwnedCollectionBranch', j.json_reference_custom_naming ->> 'CustomName', CAST(j.json_reference_custom_naming #>> ARRAY['Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸','ユニコーンFraction一角獣']::text[] AS double precision) FROM "JsonEntitiesCustomNaming" AS j """); } @@ -560,14 +661,14 @@ public override async Task Project_json_entity_FirstOrDefault_subquery(bool asyn AssertSql( """ -SELECT t.c, t."Id" +SELECT j1.c, j1."Id" FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j0."OwnedReferenceRoot" -> 'OwnedReferenceBranch' AS c, j0."Id" FROM "JsonEntitiesBasic" AS j0 ORDER BY j0."Id" NULLS FIRST LIMIT 1 -) AS t ON TRUE +) AS j1 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -602,14 +703,14 @@ public override async Task Project_json_entity_FirstOrDefault_subquery_deduplica AssertSql( """ -SELECT t.c, t."Id", t.c0, t."Id0", t.c1, t.c2, t.c3, t.c4 +SELECT j1.c, j1."Id", j1.c0, j1."Id0", j1.c1, j1.c2, j1.c3, j1.c4 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."OwnedReferenceRoot" -> 'OwnedCollectionBranch' AS c, j."Id", j0."OwnedReferenceRoot" AS c0, j0."Id" AS "Id0", j0."OwnedReferenceRoot" -> 'OwnedReferenceBranch' AS c1, j0."OwnedReferenceRoot" ->> 'Name' AS c2, CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Enum}' AS integer) AS c3, 1 AS c4 FROM "JsonEntitiesBasic" AS j0 ORDER BY j0."Id" NULLS FIRST LIMIT 1 -) AS t ON TRUE +) AS j1 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -620,14 +721,14 @@ public override async Task Project_json_entity_FirstOrDefault_subquery_deduplica AssertSql( """ -SELECT t.c, t."Id", t.c0, t."Id0", t.c1, t.c2, t.c3, t.c4 +SELECT j1.c, j1."Id", j1.c0, j1."Id0", j1.c1, j1.c2, j1.c3, j1.c4 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."OwnedReferenceRoot" -> 'OwnedCollectionBranch' AS c, j."Id", j0."OwnedReferenceRoot" AS c0, j0."Id" AS "Id0", j0."OwnedReferenceRoot" -> 'OwnedReferenceBranch' AS c1, j0."OwnedReferenceRoot" ->> 'Name' AS c2, CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Enum}' AS integer) AS c3, 1 AS c4 FROM "JsonEntitiesBasic" AS j0 ORDER BY j0."Id" NULLS FIRST LIMIT 1 -) AS t ON TRUE +) AS j1 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -638,14 +739,14 @@ public override async Task Project_json_entity_FirstOrDefault_subquery_deduplica AssertSql( """ -SELECT t.c, t."Id", t.c0 +SELECT j1.c, j1."Id", j1.c0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."OwnedReferenceRoot" -> 'OwnedCollectionBranch' AS c, j."Id", 1 AS c0 FROM "JsonEntitiesBasic" AS j0 ORDER BY j0."Id" NULLS FIRST LIMIT 1 -) AS t ON TRUE +) AS j1 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1032,16 +1133,7 @@ public override async Task Json_collection_Any_with_predicate(bool async) FROM "JsonEntitiesBasic" AS j WHERE EXISTS ( SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( - "Date" timestamp without time zone, - "Enum" integer, - "Enums" integer[], - "Fraction" numeric(18,2), - "NullableEnum" integer, - "NullableEnums" integer[], - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o + FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c1_r') """); } @@ -1059,14 +1151,11 @@ SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, "Enum" integer, - "Enums" integer[], "Fraction" numeric(18,2), - "NullableEnum" integer, - "NullableEnums" integer[], - "OwnedCollectionLeaf" jsonb, + "Id" integer, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o - WHERE o."Enum" = 2 + WHERE o."Enum" = -3 LIMIT 1 OFFSET 0) = 'e1_r_c2_r' """); } @@ -1080,21 +1169,12 @@ public override async Task Json_collection_Skip(bool async) SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" FROM "JsonEntitiesBasic" AS j WHERE ( - SELECT t.c + SELECT o0.c FROM ( - SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c, o.ordinality - FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( - "Date" timestamp without time zone, - "Enum" integer, - "Enums" integer[], - "Fraction" numeric(18,2), - "NullableEnum" integer, - "NullableEnums" integer[], - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o + SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c + FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o OFFSET 1 - ) AS t + ) AS o0 LIMIT 1 OFFSET 0) = 'e1_r_c2_r' """); } @@ -1108,23 +1188,20 @@ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" FROM "JsonEntitiesBasic" AS j WHERE ( - SELECT t.c + SELECT o0.c FROM ( - SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c, o.ordinality, o."Date" AS c0 + SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c, o."Date" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, "Enum" integer, - "Enums" integer[], "Fraction" numeric(18,2), - "NullableEnum" integer, - "NullableEnums" integer[], - "OwnedCollectionLeaf" jsonb, + "Id" integer, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o ORDER BY o."Date" DESC NULLS LAST OFFSET 1 - ) AS t - ORDER BY t.c0 DESC NULLS LAST + ) AS o0 + ORDER BY o0.c0 DESC NULLS LAST LIMIT 1 OFFSET 0) = 'e1_r_c1_r' """); } @@ -1140,19 +1217,20 @@ public override async Task Json_collection_Distinct_Count_with_predicate(bool as WHERE ( SELECT count(*)::int FROM ( - SELECT DISTINCT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf" AS c, o."OwnedReferenceLeaf" AS c0 + SELECT DISTINCT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."Id" AS "Id0", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf" AS c, o."OwnedReferenceLeaf" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c2_r' - ) AS t) = 1 + ) AS o0) = 1 """); } @@ -1166,14 +1244,7 @@ public override async Task Json_collection_within_collection_Count(bool async) FROM "JsonEntitiesBasic" AS j WHERE EXISTS ( SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o WHERE ( SELECT count(*)::int FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( @@ -1181,6 +1252,7 @@ FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, @@ -1198,6 +1270,7 @@ public override async Task Json_collection_in_projection_with_composition_count( SELECT ( SELECT count(*)::int FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1219,12 +1292,9 @@ public override async Task Json_collection_in_projection_with_anonymous_projecti SELECT j."Id", o."Name", o."Number", o.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb + "Number" integer )) WITH ORDINALITY AS o ON TRUE ORDER BY j."Id" NULLS FIRST """); @@ -1236,20 +1306,17 @@ public override async Task Json_collection_in_projection_with_composition_where_ AssertSql( """ -SELECT j."Id", t."Name", t."Number", t.ordinality +SELECT j."Id", o0."Name", o0."Number", o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o."Name", o."Number", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb + "Number" integer )) WITH ORDINALITY AS o WHERE o."Name" = 'Foo' -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1260,20 +1327,19 @@ public override async Task Json_collection_in_projection_with_composition_where_ AssertSql( """ -SELECT j."Id", t."Names", t."Numbers", t.ordinality +SELECT j."Id", o0."Names", o0."Numbers", o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o."Names", o."Numbers", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb + "Numbers" integer[] )) WITH ORDINALITY AS o WHERE o."Name" = 'Foo' -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1284,11 +1350,12 @@ public override async Task Json_collection_filter_in_projection(bool async) AssertSql( """ -SELECT j."Id", t."Id", t."Name", t."Names", t."Number", t."Numbers", t.c, t.c0, t.ordinality +SELECT j."Id", o0."Id", o0."Id0", o0."Name", o0."Names", o0."Number", o0."Numbers", o0.c, o0.c0, o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT j."Id", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality + SELECT j."Id", o."Id" AS "Id0", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1297,7 +1364,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o WHERE o."Name" <> 'Foo' OR o."Name" IS NULL -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1308,34 +1375,28 @@ public override async Task Json_nested_collection_filter_in_projection(bool asyn AssertSql( """ -SELECT j."Id", t0.ordinality, t0."Id", t0."Date", t0."Enum", t0."Enums", t0."Fraction", t0."NullableEnum", t0."NullableEnums", t0.c, t0.c0, t0.ordinality0 +SELECT j."Id", s.ordinality, s."Id", s."Date", s."Enum", s."Enums", s."Fraction", s."Id0", s."NullableEnum", s."NullableEnums", s.c, s.c0, s.ordinality0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT o.ordinality, t."Id", t."Date", t."Enum", t."Enums", t."Fraction", t."NullableEnum", t."NullableEnums", t.c, t.c0, t.ordinality AS ordinality0 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o + SELECT o.ordinality, o1."Id", o1."Date", o1."Enum", o1."Enums", o1."Fraction", o1."Id0", o1."NullableEnum", o1."NullableEnums", o1.c, o1.c0, o1.ordinality AS ordinality0 + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o LEFT JOIN LATERAL ( - SELECT j."Id", o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0."NullableEnum", o0."NullableEnums", o0."OwnedCollectionLeaf" AS c, o0."OwnedReferenceLeaf" AS c0, o0.ordinality + SELECT j."Id", o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0."Id" AS "Id0", o0."NullableEnum", o0."NullableEnums", o0."OwnedCollectionLeaf" AS c, o0."OwnedReferenceLeaf" AS c0, o0.ordinality FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o0 WHERE o0."Date" <> TIMESTAMP '2000-01-01T00:00:00' - ) AS t ON TRUE -) AS t0 ON TRUE -ORDER BY j."Id" NULLS FIRST, t0.ordinality NULLS FIRST + ) AS o1 ON TRUE +) AS s ON TRUE +ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST """); } @@ -1345,30 +1406,22 @@ public override async Task Json_nested_collection_anonymous_projection_in_projec AssertSql( """ -SELECT j."Id", t.ordinality, t.c, t.c0, t.c1, t.c2, t.c3, t."Id", t.c4, t.ordinality0 +SELECT j."Id", s.ordinality, s.c, s.c0, s.c1, s.c2, s.c3, s."Id", s.c4, s.ordinality0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o.ordinality, o0."Date" AS c, o0."Enum" AS c0, o0."Enums" AS c1, o0."Fraction" AS c2, o0."OwnedReferenceLeaf" AS c3, j."Id", o0."OwnedCollectionLeaf" AS c4, o0.ordinality AS ordinality0 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), - "NullableEnum" integer, - "NullableEnums" integer[], + "Id" integer, "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o0 ON TRUE -) AS t ON TRUE -ORDER BY j."Id" NULLS FIRST, t.ordinality NULLS FIRST +) AS s ON TRUE +ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST """); } @@ -1378,11 +1431,12 @@ public override async Task Json_collection_skip_take_in_projection(bool async) AssertSql( """ -SELECT j."Id", t."Id", t."Name", t."Names", t."Number", t."Numbers", t.c, t.c0, t.ordinality +SELECT j."Id", o0."Id", o0."Id0", o0."Name", o0."Names", o0."Number", o0."Numbers", o0.c, o0.c0, o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT j."Id", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality, o."Name" AS c1 + SELECT j."Id", o."Id" AS "Id0", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0, o.ordinality, o."Name" AS c1 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1392,8 +1446,8 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( )) WITH ORDINALITY AS o ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 -) AS t ON TRUE -ORDER BY j."Id" NULLS FIRST, t.c1 NULLS FIRST +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c1 NULLS FIRST """); } @@ -1403,11 +1457,12 @@ public override async Task Json_collection_skip_take_in_projection_project_into_ AssertSql( """ -SELECT j."Id", t.c, t.c0, t.c1, t.c2, t.c3, t."Id", t.c4, t.ordinality +SELECT j."Id", o0.c, o0.c0, o0.c1, o0.c2, o0.c3, o0."Id", o0.c4, o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o."Name" AS c, o."Names" AS c0, o."Number" AS c1, o."Numbers" AS c2, o."OwnedCollectionBranch" AS c3, j."Id", o."OwnedReferenceBranch" AS c4, o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1417,8 +1472,8 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( )) WITH ORDINALITY AS o ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 -) AS t ON TRUE -ORDER BY j."Id" NULLS FIRST, t.c NULLS FIRST +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c NULLS FIRST """); } @@ -1428,22 +1483,20 @@ public override async Task Json_collection_skip_take_in_projection_with_json_ref AssertSql( """ -SELECT j."Id", t.c, t."Id", t.ordinality +SELECT j."Id", o0.c, o0."Id", o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o."OwnedReferenceBranch" AS c, j."Id", o.ordinality, o."Name" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, - "Names" text[], "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o ORDER BY o."Name" NULLS FIRST LIMIT 5 OFFSET 1 -) AS t ON TRUE -ORDER BY j."Id" NULLS FIRST, t.c0 NULLS FIRST +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c0 NULLS FIRST """); } @@ -1453,11 +1506,12 @@ public override async Task Json_collection_distinct_in_projection(bool async) AssertSql( """ -SELECT j."Id", t."Id", t."Name", t."Names", t."Number", t."Numbers", t.c, t.c0 +SELECT j."Id", o0."Id", o0."Id0", o0."Name", o0."Names", o0."Number", o0."Numbers", o0.c, o0.c0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT DISTINCT j."Id", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0 + SELECT DISTINCT j."Id", o."Id" AS "Id0", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch" AS c, o."OwnedReferenceBranch" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1465,8 +1519,8 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( "OwnedCollectionBranch" jsonb, "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o -) AS t ON TRUE -ORDER BY j."Id" NULLS FIRST, t."Name" NULLS FIRST, t."Names" NULLS FIRST, t."Number" NULLS FIRST +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0."Id0" NULLS FIRST, o0."Name" NULLS FIRST, o0."Names" NULLS FIRST, o0."Number" NULLS FIRST """); } @@ -1483,13 +1537,13 @@ public override async Task Json_collection_leaf_filter_in_projection(bool async) AssertSql( """ -SELECT j."Id", t."Id", t."SomethingSomething", t.ordinality +SELECT j."Id", o0."Id", o0."SomethingSomething", o0.ordinality FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."Id", o."SomethingSomething", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}') AS ("SomethingSomething" text)) WITH ORDINALITY AS o WHERE o."SomethingSomething" <> 'Baz' OR o."SomethingSomething" IS NULL -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1500,16 +1554,17 @@ public override async Task Json_multiple_collection_projections(bool async) AssertSql( """ -SELECT j."Id", t."Id", t."SomethingSomething", t.ordinality, t0."Id", t0."Name", t0."Names", t0."Number", t0."Numbers", t0.c, t0.c0, t1.ordinality, t1."Id", t1."Date", t1."Enum", t1."Enums", t1."Fraction", t1."NullableEnum", t1."NullableEnums", t1.c, t1.c0, t1.ordinality0, j0."Id", j0."Name", j0."ParentId" +SELECT j."Id", o4."Id", o4."SomethingSomething", o4.ordinality, o1."Id", o1."Id0", o1."Name", o1."Names", o1."Number", o1."Numbers", o1.c, o1.c0, s.ordinality, s."Id", s."Date", s."Enum", s."Enums", s."Fraction", s."Id0", s."NullableEnum", s."NullableEnums", s.c, s.c0, s.ordinality0, j0."Id", j0."Name", j0."ParentId" FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."Id", o."SomethingSomething", o.ordinality FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}') AS ("SomethingSomething" text)) WITH ORDINALITY AS o WHERE o."SomethingSomething" <> 'Baz' OR o."SomethingSomething" IS NULL -) AS t ON TRUE +) AS o4 ON TRUE LEFT JOIN LATERAL ( - SELECT DISTINCT j."Id", o0."Name", o0."Names", o0."Number", o0."Numbers", o0."OwnedCollectionBranch" AS c, o0."OwnedReferenceBranch" AS c0 + SELECT DISTINCT j."Id", o0."Id" AS "Id0", o0."Name", o0."Names", o0."Number", o0."Numbers", o0."OwnedCollectionBranch" AS c, o0."OwnedReferenceBranch" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1517,34 +1572,28 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( "OwnedCollectionBranch" jsonb, "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o0 -) AS t0 ON TRUE +) AS o1 ON TRUE LEFT JOIN LATERAL ( - SELECT o1.ordinality, t2."Id", t2."Date", t2."Enum", t2."Enums", t2."Fraction", t2."NullableEnum", t2."NullableEnums", t2.c, t2.c0, t2.ordinality AS ordinality0 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o1 + SELECT o2.ordinality, o5."Id", o5."Date", o5."Enum", o5."Enums", o5."Fraction", o5."Id0", o5."NullableEnum", o5."NullableEnums", o5.c, o5.c0, o5.ordinality AS ordinality0 + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o2 LEFT JOIN LATERAL ( - SELECT j."Id", o2."Date", o2."Enum", o2."Enums", o2."Fraction", o2."NullableEnum", o2."NullableEnums", o2."OwnedCollectionLeaf" AS c, o2."OwnedReferenceLeaf" AS c0, o2.ordinality - FROM ROWS FROM (jsonb_to_recordset(o1."OwnedCollectionBranch") AS ( + SELECT j."Id", o3."Date", o3."Enum", o3."Enums", o3."Fraction", o3."Id" AS "Id0", o3."NullableEnum", o3."NullableEnums", o3."OwnedCollectionLeaf" AS c, o3."OwnedReferenceLeaf" AS c0, o3.ordinality + FROM ROWS FROM (jsonb_to_recordset(o2."OwnedCollectionBranch") AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o2 - WHERE o2."Date" <> TIMESTAMP '2000-01-01T00:00:00' - ) AS t2 ON TRUE -) AS t1 ON TRUE + )) WITH ORDINALITY AS o3 + WHERE o3."Date" <> TIMESTAMP '2000-01-01T00:00:00' + ) AS o5 ON TRUE +) AS s ON TRUE LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST, t.ordinality NULLS FIRST, t0."Name" NULLS FIRST, t0."Names" NULLS FIRST, t0."Number" NULLS FIRST, t0."Numbers" NULLS FIRST, t1.ordinality NULLS FIRST, t1.ordinality0 NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o4.ordinality NULLS FIRST, o1."Id0" NULLS FIRST, o1."Name" NULLS FIRST, o1."Names" NULLS FIRST, o1."Number" NULLS FIRST, o1."Numbers" NULLS FIRST, s.ordinality NULLS FIRST, s.ordinality0 NULLS FIRST """); } @@ -1554,23 +1603,24 @@ public override async Task Json_branch_collection_distinct_and_other_collection( AssertSql( """ -SELECT j."Id", t."Id", t."Date", t."Enum", t."Enums", t."Fraction", t."NullableEnum", t."NullableEnums", t.c, t.c0, j0."Id", j0."Name", j0."ParentId" +SELECT j."Id", o0."Id", o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0."Id0", o0."NullableEnum", o0."NullableEnums", o0.c, o0.c0, j0."Id", j0."Name", j0."ParentId" FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( - SELECT DISTINCT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf" AS c, o."OwnedReferenceLeaf" AS c0 + SELECT DISTINCT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."Id" AS "Id0", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf" AS c, o."OwnedReferenceLeaf" AS c0 FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o -) AS t ON TRUE +) AS o0 ON TRUE LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST, t."Date" NULLS FIRST, t."Enum" NULLS FIRST, t."Enums" NULLS FIRST, t."Fraction" NULLS FIRST, t."NullableEnum" NULLS FIRST, t."NullableEnums" NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o0."Date" NULLS FIRST, o0."Enum" NULLS FIRST, o0."Enums" NULLS FIRST, o0."Fraction" NULLS FIRST, o0."Id0" NULLS FIRST, o0."NullableEnum" NULLS FIRST, o0."NullableEnums" NULLS FIRST """); } @@ -1580,14 +1630,14 @@ public override async Task Json_leaf_collection_distinct_and_other_collection(bo AssertSql( """ -SELECT j."Id", t."Id", t."SomethingSomething", j0."Id", j0."Name", j0."ParentId" +SELECT j."Id", o0."Id", o0."SomethingSomething", j0."Id", j0."Name", j0."ParentId" FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT DISTINCT j."Id", o."SomethingSomething" FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}') AS ("SomethingSomething" text)) WITH ORDINALITY AS o -) AS t ON TRUE +) AS o0 ON TRUE LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST, t."SomethingSomething" NULLS FIRST +ORDER BY j."Id" NULLS FIRST, o0."SomethingSomething" NULLS FIRST """); } @@ -1597,9 +1647,10 @@ public override async Task Json_collection_SelectMany(bool async) AssertSql( """ -SELECT j."Id", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch", o."OwnedReferenceBranch" +SELECT j."Id", o."Id", o."Name", o."Names", o."Number", o."Numbers", o."OwnedCollectionBranch", o."OwnedReferenceBranch" FROM "JsonEntitiesBasic" AS j JOIN LATERAL ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1616,13 +1667,14 @@ public override async Task Json_nested_collection_SelectMany(bool async) AssertSql( """ -SELECT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf", o."OwnedReferenceLeaf" +SELECT j."Id", o."Date", o."Enum", o."Enums", o."Fraction", o."Id", o."NullableEnum", o."NullableEnums", o."OwnedCollectionLeaf", o."OwnedReferenceLeaf" FROM "JsonEntitiesBasic" AS j JOIN LATERAL ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( "Date" timestamp without time zone, "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, @@ -1635,7 +1687,12 @@ public override async Task Json_collection_of_primitives_SelectMany(bool async) { await base.Json_collection_of_primitives_SelectMany(async); - AssertSql(""); + AssertSql( + """ +SELECT n.value +FROM "JsonEntitiesBasic" AS j +JOIN LATERAL unnest((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(j."OwnedReferenceRoot" -> 'Names') WITH ORDINALITY AS t(element) ORDER BY ordinality))) AS n(value) ON TRUE +"""); } public override async Task Json_collection_of_primitives_index_used_in_predicate(bool async) @@ -1701,6 +1758,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot" #> ARRAY[@__prm_0,'Ow "Enum" integer, "Enums" integer[], "Fraction" numeric(18,2), + "Id" integer, "NullableEnum" integer, "NullableEnums" integer[], "OwnedCollectionLeaf" jsonb, @@ -1752,20 +1810,13 @@ public override async Task Json_collection_Select_entity_in_anonymous_object_Ele AssertSql( """ -SELECT t.c, t."Id", t.c0 +SELECT o0.c, o0."Id", o0.c0 FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT o."OwnedReferenceBranch" AS c, j."Id", 1 AS c0 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Name" text, - "Names" text[], - "Number" integer, - "Numbers" integer[], - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedReferenceBranch" jsonb)) WITH ORDINALITY AS o LIMIT 1 OFFSET 0 -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY j."Id" NULLS FIRST """); } @@ -1776,11 +1827,12 @@ public override async Task Json_collection_Select_entity_with_initializer_Elemen AssertSql( """ -SELECT t."Id", t.c +SELECT o0."Id", o0.c FROM "JsonEntitiesBasic" AS j LEFT JOIN LATERAL ( SELECT j."Id", 1 AS c FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( + "Id" integer, "Name" text, "Names" text[], "Number" integer, @@ -1789,7 +1841,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( "OwnedReferenceBranch" jsonb )) WITH ORDINALITY AS o LIMIT 1 OFFSET 0 -) AS t ON TRUE +) AS o0 ON TRUE """); } @@ -2030,12 +2082,12 @@ public override async Task Group_by_on_json_scalar(bool async) AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT j0."Key", count(*)::int AS "Count" FROM ( SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" FROM "JsonEntitiesBasic" AS j -) AS t -GROUP BY t."Key" +) AS j0 +GROUP BY j0."Key" """); } @@ -2045,12 +2097,12 @@ public override async Task Group_by_on_json_scalar_using_collection_indexer(bool AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT j0."Key", count(*)::int AS "Count" FROM ( SELECT j."OwnedCollectionRoot" #>> '{0,Name}' AS "Key" FROM "JsonEntitiesBasic" AS j -) AS t -GROUP BY t."Key" +) AS j0 +GROUP BY j0."Key" """); } @@ -2060,26 +2112,26 @@ public override async Task Group_by_First_on_json_scalar(bool async) AssertSql( """ -SELECT t1."Id", t1."EntityBasicId", t1."Name", t1.c, t1.c0 +SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 FROM ( - SELECT t."Key" + SELECT j0."Key" FROM ( SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" FROM "JsonEntitiesBasic" AS j - ) AS t - GROUP BY t."Key" -) AS t0 + ) AS j0 + GROUP BY j0."Key" +) AS j3 LEFT JOIN ( - SELECT t2."Id", t2."EntityBasicId", t2."Name", t2.c AS c, t2.c0 AS c0, t2."Key" + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" FROM ( - SELECT t3."Id", t3."EntityBasicId", t3."Name", t3.c AS c, t3.c0 AS c0, t3."Key", ROW_NUMBER() OVER(PARTITION BY t3."Key" ORDER BY t3."Id" NULLS FIRST) AS row + SELECT j1."Id", j1."EntityBasicId", j1."Name", j1.c AS c, j1.c0 AS c0, j1."Key", ROW_NUMBER() OVER(PARTITION BY j1."Key" ORDER BY j1."Id" NULLS FIRST) AS row FROM ( - SELECT j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot" AS c, j0."OwnedReferenceRoot" AS c0, j0."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j0 - ) AS t3 - ) AS t2 - WHERE t2.row <= 1 -) AS t1 ON t0."Key" = t1."Key" + SELECT j2."Id", j2."EntityBasicId", j2."Name", j2."OwnedCollectionRoot" AS c, j2."OwnedReferenceRoot" AS c0, j2."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j2 + ) AS j1 + ) AS j4 + WHERE j4.row <= 1 +) AS j5 ON j3."Key" = j5."Key" """); } @@ -2089,26 +2141,26 @@ public override async Task Group_by_FirstOrDefault_on_json_scalar(bool async) AssertSql( """ -SELECT t1."Id", t1."EntityBasicId", t1."Name", t1.c, t1.c0 +SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 FROM ( - SELECT t."Key" + SELECT j0."Key" FROM ( SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" FROM "JsonEntitiesBasic" AS j - ) AS t - GROUP BY t."Key" -) AS t0 + ) AS j0 + GROUP BY j0."Key" +) AS j3 LEFT JOIN ( - SELECT t2."Id", t2."EntityBasicId", t2."Name", t2.c AS c, t2.c0 AS c0, t2."Key" + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" FROM ( - SELECT t3."Id", t3."EntityBasicId", t3."Name", t3.c AS c, t3.c0 AS c0, t3."Key", ROW_NUMBER() OVER(PARTITION BY t3."Key" ORDER BY t3."Id" NULLS FIRST) AS row + SELECT j1."Id", j1."EntityBasicId", j1."Name", j1.c AS c, j1.c0 AS c0, j1."Key", ROW_NUMBER() OVER(PARTITION BY j1."Key" ORDER BY j1."Id" NULLS FIRST) AS row FROM ( - SELECT j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot" AS c, j0."OwnedReferenceRoot" AS c0, j0."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j0 - ) AS t3 - ) AS t2 - WHERE t2.row <= 1 -) AS t1 ON t0."Key" = t1."Key" + SELECT j2."Id", j2."EntityBasicId", j2."Name", j2."OwnedCollectionRoot" AS c, j2."OwnedReferenceRoot" AS c0, j2."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j2 + ) AS j1 + ) AS j4 + WHERE j4.row <= 1 +) AS j5 ON j3."Key" = j5."Key" """); } @@ -2118,27 +2170,27 @@ public override async Task Group_by_Skip_Take_on_json_scalar(bool async) AssertSql( """ -SELECT t0."Key", t1."Id", t1."EntityBasicId", t1."Name", t1.c, t1.c0 +SELECT j3."Key", j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 FROM ( - SELECT t."Key" + SELECT j0."Key" FROM ( SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" FROM "JsonEntitiesBasic" AS j - ) AS t - GROUP BY t."Key" -) AS t0 + ) AS j0 + GROUP BY j0."Key" +) AS j3 LEFT JOIN ( - SELECT t2."Id", t2."EntityBasicId", t2."Name", t2.c, t2.c0, t2."Key" + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c, j4.c0, j4."Key" FROM ( - SELECT t3."Id", t3."EntityBasicId", t3."Name", t3.c AS c, t3.c0 AS c0, t3."Key", ROW_NUMBER() OVER(PARTITION BY t3."Key" ORDER BY t3."Id" NULLS FIRST) AS row + SELECT j1."Id", j1."EntityBasicId", j1."Name", j1.c AS c, j1.c0 AS c0, j1."Key", ROW_NUMBER() OVER(PARTITION BY j1."Key" ORDER BY j1."Id" NULLS FIRST) AS row FROM ( - SELECT j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot" AS c, j0."OwnedReferenceRoot" AS c0, j0."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j0 - ) AS t3 - ) AS t2 - WHERE 1 < t2.row AND t2.row <= 6 -) AS t1 ON t0."Key" = t1."Key" -ORDER BY t0."Key" NULLS FIRST, t1."Key" NULLS FIRST, t1."Id" NULLS FIRST + SELECT j2."Id", j2."EntityBasicId", j2."Name", j2."OwnedCollectionRoot" AS c, j2."OwnedReferenceRoot" AS c0, j2."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j2 + ) AS j1 + ) AS j4 + WHERE 1 < j4.row AND j4.row <= 6 +) AS j5 ON j3."Key" = j5."Key" +ORDER BY j3."Key" NULLS FIRST, j5."Key" NULLS FIRST, j5."Id" NULLS FIRST """); } @@ -2157,18 +2209,18 @@ public override async Task Group_by_json_scalar_Skip_First_project_json_scalar(b AssertSql( """ SELECT ( - SELECT CAST(t0.c0 #>> '{OwnedReferenceBranch,Enum}' AS integer) + SELECT CAST(j1.c0 #>> '{OwnedReferenceBranch,Enum}' AS integer) FROM ( - SELECT j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot" AS c, j0."OwnedReferenceRoot" AS c0, j0."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j0 - ) AS t0 - WHERE t."Key" = t0."Key" OR (t."Key" IS NULL AND t0."Key" IS NULL) + SELECT j2."OwnedReferenceRoot" AS c0, j2."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j2 + ) AS j1 + WHERE j0."Key" = j1."Key" OR (j0."Key" IS NULL AND j1."Key" IS NULL) LIMIT 1) FROM ( SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" FROM "JsonEntitiesBasic" AS j -) AS t -GROUP BY t."Key" +) AS j0 +GROUP BY j0."Key" """); } @@ -2343,7 +2395,7 @@ public override async Task Json_all_types_entity_projection(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j """); } @@ -2376,7 +2428,7 @@ public override async Task Json_boolean_predicate(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE CAST(j."Reference" ->> 'TestBoolean' AS boolean) """); @@ -2388,7 +2440,7 @@ public override async Task Json_boolean_predicate_negated(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) """); @@ -2422,7 +2474,7 @@ public override async Task Json_predicate_on_default_string(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (j."Reference" ->> 'TestDefaultString') <> 'MyDefaultStringInReference1' OR (j."Reference" ->> 'TestDefaultString') IS NULL """); @@ -2434,7 +2486,7 @@ public override async Task Json_predicate_on_max_length_string(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (j."Reference" ->> 'TestMaxLengthString') <> 'Foo' OR (j."Reference" ->> 'TestMaxLengthString') IS NULL """); @@ -2446,7 +2498,7 @@ public override async Task Json_predicate_on_string_condition(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE CASE WHEN NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) THEN j."Reference" ->> 'TestMaxLengthString' @@ -2461,19 +2513,31 @@ public override async Task Json_predicate_on_byte(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestByte' AS smallint)) <> 3 OR (CAST(j."Reference" ->> 'TestByte' AS smallint)) IS NULL """); } + public override async Task Json_predicate_on_byte_array(bool async) + { + await base.Json_predicate_on_byte_array(async); + + AssertSql( + """ +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +FROM "JsonEntitiesAllTypes" AS j +WHERE (decode(j."Reference" ->> 'TestByteArray', 'base64')) <> BYTEA E'\\x010203' OR (decode(j."Reference" ->> 'TestByteArray', 'base64')) IS NULL +"""); + } + public override async Task Json_predicate_on_character(bool async) { await base.Json_predicate_on_character(async); AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestCharacter' AS character(1))) <> 'z' OR (CAST(j."Reference" ->> 'TestCharacter' AS character(1))) IS NULL """); @@ -2485,7 +2549,7 @@ public override async Task Json_predicate_on_datetime(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) <> TIMESTAMP '2000-01-03T00:00:00' OR (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) IS NULL """); @@ -2497,7 +2561,7 @@ public override async Task Json_predicate_on_datetimeoffset(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) <> TIMESTAMPTZ '2000-01-04T00:00:00+03:02' OR (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) IS NULL """); @@ -2509,7 +2573,7 @@ public override async Task Json_predicate_on_decimal(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) <> 1.35 OR (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) IS NULL """); @@ -2521,7 +2585,7 @@ public override async Task Json_predicate_on_double(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestDouble' AS double precision)) <> 33.25 OR (CAST(j."Reference" ->> 'TestDouble' AS double precision)) IS NULL """); @@ -2533,9 +2597,9 @@ public override async Task Json_predicate_on_enum(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestEnum' AS integer)) <> 1 OR (CAST(j."Reference" ->> 'TestEnum' AS integer)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestEnum' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestEnum' AS integer)) IS NULL """); } @@ -2545,9 +2609,9 @@ public override async Task Json_predicate_on_enumwithintconverter(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) <> -3 OR (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) IS NULL """); } @@ -2557,7 +2621,7 @@ public override async Task Json_predicate_on_guid(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestGuid' AS uuid)) <> '00000000-0000-0000-0000-000000000000' OR (CAST(j."Reference" ->> 'TestGuid' AS uuid)) IS NULL """); @@ -2569,7 +2633,7 @@ public override async Task Json_predicate_on_int16(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestInt16' AS smallint)) <> 3 OR (CAST(j."Reference" ->> 'TestInt16' AS smallint)) IS NULL """); @@ -2581,7 +2645,7 @@ public override async Task Json_predicate_on_int32(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestInt32' AS integer)) <> 33 OR (CAST(j."Reference" ->> 'TestInt32' AS integer)) IS NULL """); @@ -2593,7 +2657,7 @@ public override async Task Json_predicate_on_int64(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestInt64' AS bigint)) <> 333 OR (CAST(j."Reference" ->> 'TestInt64' AS bigint)) IS NULL """); @@ -2605,9 +2669,9 @@ public override async Task Json_predicate_on_nullableenum1(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) <> 0 OR (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) <> -1 OR (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NULL """); } @@ -2617,7 +2681,7 @@ public override async Task Json_predicate_on_nullableenum2(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NOT NULL """); @@ -2629,9 +2693,9 @@ public override async Task Json_predicate_on_nullableenumwithconverter1(bool asy AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) <> 1 OR (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NULL """); } @@ -2641,7 +2705,7 @@ public override async Task Json_predicate_on_nullableenumwithconverter2(bool asy AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NOT NULL """); @@ -2653,7 +2717,7 @@ public override async Task Json_predicate_on_nullableenumwithconverterthathandle AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') <> 'One' OR (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') IS NULL """); @@ -2675,7 +2739,7 @@ public override async Task Json_predicate_on_nullableint321(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NULL """); @@ -2687,7 +2751,7 @@ public override async Task Json_predicate_on_nullableint322(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NOT NULL """); @@ -2699,7 +2763,7 @@ public override async Task Json_predicate_on_signedbyte(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) <> 100 OR (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) IS NULL """); @@ -2711,7 +2775,7 @@ public override async Task Json_predicate_on_single(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestSingle' AS real)) <> 10.4 OR (CAST(j."Reference" ->> 'TestSingle' AS real)) IS NULL """); @@ -2723,7 +2787,7 @@ public override async Task Json_predicate_on_timespan(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) <> INTERVAL '03:02:00' OR (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) IS NULL """); @@ -2735,7 +2799,7 @@ public override async Task Json_predicate_on_dateonly(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestDateOnly' AS date)) <> DATE '0003-02-01' OR (CAST(j."Reference" ->> 'TestDateOnly' AS date)) IS NULL """); @@ -2747,7 +2811,7 @@ public override async Task Json_predicate_on_timeonly(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestTimeOnly' AS time without time zone)) <> TIME '03:02:00' OR (CAST(j."Reference" ->> 'TestTimeOnly' AS time without time zone)) IS NULL """); @@ -2759,7 +2823,7 @@ public override async Task Json_predicate_on_unisgnedint16(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) IS NULL """); @@ -2771,7 +2835,7 @@ public override async Task Json_predicate_on_unsignedint32(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) <> 1000 OR (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) IS NULL """); @@ -2783,7 +2847,7 @@ public override async Task Json_predicate_on_unsignedint64(bool async) AssertSql( """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) <> 10000.0 OR (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) IS NULL """); @@ -2897,8 +2961,6 @@ public override async Task Json_predicate_on_string_Y_N_converted_to_bool(bool a """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_basic(bool async) { await base.FromSql_on_entity_with_json_basic(async); @@ -2912,8 +2974,6 @@ public override async Task FromSql_on_entity_with_json_basic(bool async) """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_project_json_reference(bool async) { await base.FromSql_on_entity_with_json_project_json_reference(async); @@ -2927,8 +2987,6 @@ public override async Task FromSql_on_entity_with_json_project_json_reference(bo """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_project_json_collection(bool async) { await base.FromSql_on_entity_with_json_project_json_collection(async); @@ -2942,8 +3000,6 @@ public override async Task FromSql_on_entity_with_json_project_json_collection(b """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_on_base(bool async) { await base.FromSql_on_entity_with_json_inheritance_on_base(async); @@ -2957,8 +3013,6 @@ public override async Task FromSql_on_entity_with_json_inheritance_on_base(bool """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_on_derived(bool async) { await base.FromSql_on_entity_with_json_inheritance_on_derived(async); @@ -2973,8 +3027,6 @@ public override async Task FromSql_on_entity_with_json_inheritance_on_derived(bo """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_project_reference_on_base(bool async) { await base.FromSql_on_entity_with_json_inheritance_project_reference_on_base(async); @@ -2989,8 +3041,6 @@ ORDER BY m."Id" NULLS FIRST """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_project_reference_on_derived(bool async) { await base.FromSql_on_entity_with_json_inheritance_project_reference_on_derived(async); @@ -3006,6 +3056,336 @@ ORDER BY m."Id" NULLS FIRST """); } + public override async Task Json_projection_using_queryable_methods_on_top_of_JSON_collection_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_using_queryable_methods_on_top_of_JSON_collection_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_nested_collection_anonymous_projection_in_projection_NoTrackingWithIdentityResolution(bool async) + { + await base.Json_nested_collection_anonymous_projection_in_projection_NoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution2(bool async) + { + await base.Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution2(async); + + AssertSql( +); + } + + public override async Task + Json_projection_second_element_through_collection_element_parameter_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_parameter_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( +); + } + + public override async Task + Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( +); + } + + public override async Task + Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution2( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution2( + async); + + AssertSql( +); + } + + public override async Task + Json_projection_second_element_through_collection_element_parameter_projected_after_owner_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_parameter_projected_after_owner_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( +); + } + + public override async Task + Json_projection_second_element_through_collection_element_constant_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_constant_projected_before_owner_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( +); + } + + public override async Task Json_branch_collection_distinct_and_other_collection_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_branch_collection_distinct_and_other_collection_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_collection_SelectMany_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_collection_SelectMany_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_deduplication_with_collection_indexer_in_target_AsNoTrackingWithIdentityResolution( + bool async) + { + await base.Json_projection_deduplication_with_collection_indexer_in_target_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_nested_collection_and_element_wrong_order_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_and_element_wrong_order_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_second_element_projected_before_entire_collection_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_entire_collection_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_second_element_projected_before_owner_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_second_element_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +); + } + + public override async Task Json_projection_collection_element_and_reference_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_collection_element_and_reference_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,1}', j."OwnedReferenceRoot" -> 'OwnedReferenceBranch' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_nothing_interesting_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nothing_interesting_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."Name" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_owner_entity_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_owner_entity_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_nested_collection_anonymous_projection_of_primitives_in_projection_NoTrackingWithIdentityResolution(bool async) + { + await base.Json_nested_collection_anonymous_projection_of_primitives_in_projection_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", s.ordinality, s.c, s.c0, s.c1, s.c2, s.ordinality0 +FROM "JsonEntitiesBasic" AS j +LEFT JOIN LATERAL ( + SELECT o.ordinality, o0."Date" AS c, o0."Enum" AS c0, o0."Enums" AS c1, o0."Fraction" AS c2, o0.ordinality AS ordinality0 + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o + LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( + "Date" timestamp without time zone, + "Enum" integer, + "Enums" integer[], + "Fraction" numeric(18,2), + "Id" integer + )) WITH ORDINALITY AS o0 ON TRUE +) AS s ON TRUE +ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST +"""); + } + + public override async Task Json_projection_second_element_through_collection_element_constant_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_through_collection_element_constant_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_reference_collection_and_collection_element_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_reference_collection_and_collection_element_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedReferenceLeaf}', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task + Json_projection_second_element_through_collection_element_parameter_correctly_projected_after_owner_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_second_element_through_collection_element_parameter_correctly_projected_after_owner_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( + """ +@__prm_0='1' + +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',0,'OwnedCollectionLeaf',@__prm_0]::text[], @__prm_0 +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task + Json_projection_only_second_element_through_collection_element_constant_projected_nested_AsNoTrackingWithIdentityResolution( + bool async) + { + await base + .Json_projection_only_second_element_through_collection_element_constant_projected_nested_AsNoTrackingWithIdentityResolution( + async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_only_second_element_through_collection_element_parameter_projected_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_only_second_element_through_collection_element_parameter_projected_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +@__prm1_0='0' +@__prm2_1='1' + +SELECT j."Id", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@__prm1_0,'OwnedCollectionLeaf',@__prm2_1]::text[], @__prm1_0, @__prm2_1 +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_second_element_through_collection_element_constant_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_through_collection_element_constant_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf,1}', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,1,OwnedCollectionLeaf}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_nested_collection_and_element_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_and_element_correct_order_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_nested_collection_element_using_parameter_and_the_owner_in_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_element_using_parameter_and_the_owner_in_correct_order_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +@__prm_0='0' + +SELECT j."Id", j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@__prm_0,'OwnedCollectionLeaf',1]::text[], @__prm_0 +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_second_element_projected_before_owner_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_as_well_as_root_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,1}', j."OwnedReferenceRoot", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_second_element_projected_before_owner_nested_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_nested_as_well_as_root_AsNoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,1}', j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); @@ -3013,12 +3393,55 @@ public virtual void Check_all_tests_overridden() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class JsonQueryNpgsqlFixture : JsonQueryFixtureBase + public class JsonQueryNpgsqlFixture : JsonQueryRelationalFixture, IQueryFixtureBase { protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; private JsonQueryData _expectedData; + private readonly IReadOnlyDictionary _entityAsserters; + + public JsonQueryNpgsqlFixture() + { + var entityAsserters = base.EntityAsserters.ToDictionary(); + + entityAsserters[typeof(JsonEntityAllTypes)] = (object e, object a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityAllTypes)e; + var aa = (JsonEntityAllTypes)a; + + Assert.Equal(ee.Id, aa.Id); + + AssertAllTypes(ee.Reference, aa.Reference); + + Assert.Equal(ee.Collection?.Count ?? 0, aa.Collection?.Count ?? 0); + for (var i = 0; i < ee.Collection.Count; i++) + { + AssertAllTypes(ee.Collection[i], aa.Collection[i]); + } + } + }; + + entityAsserters[typeof(JsonOwnedAllTypes)] = (object e, object a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonOwnedAllTypes)e; + var aa = (JsonOwnedAllTypes)a; + + AssertAllTypes(ee, aa); + } + }; + + _entityAsserters = entityAsserters; + } + + IReadOnlyDictionary IQueryFixtureBase.EntityAsserters + => _entityAsserters; protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -3054,6 +3477,77 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Ignore(j => j.TestDoubleCollection); b.Ignore(j => j.TestInt16Collection); }); + + // These use collection types which are unsupported for arrays at the Npgsql level - we currently only support List/array. + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestInt64Collection); + b.Ignore(j => j.TestGuidCollection); + }); + + // Ignore nested collections - these aren't supported on PostgreSQL (no arrays of arrays). + // TODO: Remove these after syncing to 9.0.0-rc.1, and extending from the relational test base and fixture + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); + + modelBuilder.Entity().OwnsOne( + x => x.Reference, b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); + + modelBuilder.Entity().OwnsMany( + x => x.Collection, b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); } public override ISetSource GetExpectedData() @@ -3085,7 +3579,7 @@ public override ISetSource GetExpectedData() return _expectedData; } - protected override void Seed(JsonQueryContext context) + protected override async Task SeedAsync(JsonQueryContext context) { // The test data contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. @@ -3129,7 +3623,66 @@ protected override void Seed(JsonQueryContext context) dto => new DateTimeOffset(dto.Ticks - (dto.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero)).ToList(); } - context.SaveChanges(); + await context.SaveChangesAsync(); + } + + public static new void AssertAllTypes(JsonOwnedAllTypes expected, JsonOwnedAllTypes actual) + { + Assert.Equal(expected.TestDefaultString, actual.TestDefaultString); + Assert.Equal(expected.TestMaxLengthString, actual.TestMaxLengthString); + Assert.Equal(expected.TestBoolean, actual.TestBoolean); + Assert.Equal(expected.TestCharacter, actual.TestCharacter); + Assert.Equal(expected.TestDateTime, actual.TestDateTime); + Assert.Equal(expected.TestDateTimeOffset, actual.TestDateTimeOffset); + Assert.Equal(expected.TestDouble, actual.TestDouble); + Assert.Equal(expected.TestGuid, actual.TestGuid); + Assert.Equal(expected.TestInt16, actual.TestInt16); + Assert.Equal(expected.TestInt32, actual.TestInt32); + Assert.Equal(expected.TestInt64, actual.TestInt64); + Assert.Equal(expected.TestSignedByte, actual.TestSignedByte); + Assert.Equal(expected.TestSingle, actual.TestSingle); + Assert.Equal(expected.TestTimeSpan, actual.TestTimeSpan); + Assert.Equal(expected.TestDateOnly, actual.TestDateOnly); + Assert.Equal(expected.TestTimeOnly, actual.TestTimeOnly); + Assert.Equal(expected.TestUnsignedInt16, actual.TestUnsignedInt16); + Assert.Equal(expected.TestUnsignedInt32, actual.TestUnsignedInt32); + Assert.Equal(expected.TestUnsignedInt64, actual.TestUnsignedInt64); + Assert.Equal(expected.TestNullableInt32, actual.TestNullableInt32); + Assert.Equal(expected.TestEnum, actual.TestEnum); + Assert.Equal(expected.TestEnumWithIntConverter, actual.TestEnumWithIntConverter); + Assert.Equal(expected.TestNullableEnum, actual.TestNullableEnum); + Assert.Equal(expected.TestNullableEnumWithIntConverter, actual.TestNullableEnumWithIntConverter); + Assert.Equal(expected.TestNullableEnumWithConverterThatHandlesNulls, actual.TestNullableEnumWithConverterThatHandlesNulls); + + AssertPrimitiveCollection(expected.TestDefaultStringCollection, actual.TestDefaultStringCollection); + AssertPrimitiveCollection(expected.TestMaxLengthStringCollection, actual.TestMaxLengthStringCollection); + AssertPrimitiveCollection(expected.TestBooleanCollection, actual.TestBooleanCollection); + AssertPrimitiveCollection(expected.TestCharacterCollection, actual.TestCharacterCollection); + AssertPrimitiveCollection(expected.TestDateTimeCollection, actual.TestDateTimeCollection); + AssertPrimitiveCollection(expected.TestDateTimeOffsetCollection, actual.TestDateTimeOffsetCollection); + AssertPrimitiveCollection(expected.TestDoubleCollection, actual.TestDoubleCollection); + AssertPrimitiveCollection(expected.TestGuidCollection, actual.TestGuidCollection); + AssertPrimitiveCollection((IList)expected.TestInt16Collection, (IList)actual.TestInt16Collection); + AssertPrimitiveCollection(expected.TestInt32Collection, actual.TestInt32Collection); + AssertPrimitiveCollection(expected.TestInt64Collection, actual.TestInt64Collection); + AssertPrimitiveCollection(expected.TestSignedByteCollection, actual.TestSignedByteCollection); + AssertPrimitiveCollection(expected.TestSingleCollection, actual.TestSingleCollection); + AssertPrimitiveCollection(expected.TestTimeSpanCollection, actual.TestTimeSpanCollection); + AssertPrimitiveCollection(expected.TestDateOnlyCollection, actual.TestDateOnlyCollection); + AssertPrimitiveCollection(expected.TestTimeOnlyCollection, actual.TestTimeOnlyCollection); + AssertPrimitiveCollection(expected.TestUnsignedInt16Collection, actual.TestUnsignedInt16Collection); + AssertPrimitiveCollection(expected.TestUnsignedInt32Collection, actual.TestUnsignedInt32Collection); + AssertPrimitiveCollection(expected.TestUnsignedInt64Collection, actual.TestUnsignedInt64Collection); + AssertPrimitiveCollection(expected.TestNullableInt32Collection, actual.TestNullableInt32Collection); + AssertPrimitiveCollection(expected.TestEnumCollection, actual.TestEnumCollection); + AssertPrimitiveCollection(expected.TestEnumWithIntConverterCollection, actual.TestEnumWithIntConverterCollection); + AssertPrimitiveCollection(expected.TestNullableEnumCollection, actual.TestNullableEnumCollection); + AssertPrimitiveCollection( + expected.TestNullableEnumWithIntConverterCollection, actual.TestNullableEnumWithIntConverterCollection); + + // AssertPrimitiveCollection( + // expected.TestNullableEnumWithConverterThatHandlesNullsCollection, + // actual.TestNullableEnumWithConverterThatHandlesNullsCollection); } } } diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs index 5f3d7bc0e..3608a7dfd 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs @@ -52,7 +52,7 @@ public void Literal() Assert.Empty( ctx.JsonEntities.Where( - e => e.CustomerJsonb == @"{""Name"":""Test customer"",""Age"":80,""IsVip"":false,""Statistics"":null,""Orders"":null}")); + e => e.CustomerJsonb == """{"Name":"Test customer","Age":80,"IsVip":false,"Statistics":null,"Orders":null}""")); AssertSql( """ @@ -99,7 +99,7 @@ public void JsonContains_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContains(e.CustomerJsonb, element)); Assert.Equal(1, count); @@ -119,7 +119,7 @@ public void JsonContains_with_string() { using var ctx = CreateContext(); - var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContains(e.CustomerJsonb, @"{""Name"": ""Joe"", ""Age"": 25}")); + var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContains(e.CustomerJsonb, """{"Name": "Joe", "Age": 25}""")); Assert.Equal(1, count); @@ -137,7 +137,11 @@ public void JsonContains_with_string_column() using var ctx = CreateContext(); var count = ctx.JsonEntities.Count( - e => EF.Functions.JsonContains(e.CustomerJsonb, @"{""Name"": """ + e.SomeString + @""", ""Age"": 25}")); + e => EF.Functions.JsonContains(e.CustomerJsonb, """ + {"Name": " + """ + e.SomeString + """ + ", "Age": 25} + """)); Assert.Equal(1, count); @@ -154,7 +158,7 @@ public void JsonContained_with_json_element() { using var ctx = CreateContext(); - var element = JsonDocument.Parse(@"{""Name"": ""Joe"", ""Age"": 25}").RootElement; + var element = JsonDocument.Parse("""{"Name": "Joe", "Age": 25}""").RootElement; var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContained(element, e.CustomerJsonb)); Assert.Equal(1, count); @@ -174,7 +178,7 @@ public void JsonContained_with_string() { using var ctx = CreateContext(); - var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContained(@"{""Name"": ""Joe"", ""Age"": 25}", e.CustomerJsonb)); + var count = ctx.JsonEntities.Count(e => EF.Functions.JsonContained("""{"Name": "Joe", "Age": 25}""", e.CustomerJsonb)); Assert.Equal(1, count); @@ -247,71 +251,70 @@ protected JsonStringQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class JsonStringQueryContext : PoolableDbContext + public class JsonStringQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet JsonEntities { get; set; } - public JsonStringQueryContext(DbContextOptions options) - : base(options) + public static async Task SeedAsync(JsonStringQueryContext context) { - } + const string customer1 = """ - public static void Seed(JsonStringQueryContext context) - { - const string customer1 = @" { - ""Name"": ""Joe"", - ""Age"": 25, - ""IsVip"": false, - ""Statistics"": + "Name": "Joe", + "Age": 25, + "IsVip": false, + "Statistics": { - ""Visits"": 4, - ""Purchases"": 3, - ""Nested"": + "Visits": 4, + "Purchases": 3, + "Nested": { - ""SomeProperty"": 10, - ""IntArray"": [3, 4] + "SomeProperty": 10, + "IntArray": [3, 4] } }, - ""Orders"": + "Orders": [ { - ""Price"": 99.5, - ""ShippingAddress"": ""Some address 1"", - ""ShippingDate"": ""2019-10-01"" + "Price": 99.5, + "ShippingAddress": "Some address 1", + "ShippingDate": "2019-10-01" }, { - ""Price"": 23, - ""ShippingAddress"": ""Some address 2"", - ""ShippingDate"": ""2019-10-10"" + "Price": 23, + "ShippingAddress": "Some address 2", + "ShippingDate": "2019-10-10" } ] - }"; + } +"""; + + const string customer2 = """ - const string customer2 = @" { - ""Name"": ""Moe"", - ""Age"": 35, - ""IsVip"": true, - ""Statistics"": + "Name": "Moe", + "Age": 35, + "IsVip": true, + "Statistics": { - ""Visits"": 20, - ""Purchases"": 25, - ""Nested"": + "Visits": 20, + "Purchases": 25, + "Nested": { - ""SomeProperty"": 20, - ""IntArray"": [5, 6] + "SomeProperty": 20, + "IntArray": [5, 6] } }, - ""Orders"": + "Orders": [ { - ""Price"": 5, - ""ShippingAddress"": ""Moe's address"", - ""ShippingDate"": ""2019-11-03"" + "Price": 5, + "ShippingAddress": "Moe's address", + "ShippingDate": "2019-11-03" } ] - }"; + } +"""; const string array = "[1, 2, 3]"; @@ -336,7 +339,8 @@ public static void Seed(JsonStringQueryContext context) CustomerJsonb = array, CustomerJson = array }); - context.SaveChanges(); + + await context.SaveChangesAsync(); } } @@ -364,8 +368,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(JsonStringQueryContext context) - => JsonStringQueryContext.Seed(context); + protected override Task SeedAsync(JsonStringQueryContext context) + => JsonStringQueryContext.SeedAsync(context); } #endregion diff --git a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs index 1e0777b51..c09820ac1 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs @@ -502,16 +502,11 @@ protected LTreeQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class LTreeQueryContext : PoolableDbContext + public class LTreeQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet LTreeEntities { get; set; } - public LTreeQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(LTreeQueryContext context) + public static async Task SeedAsync(LTreeQueryContext context) { var ltreeEntities = new LTreeEntity[] { @@ -528,11 +523,11 @@ public static void Seed(LTreeQueryContext context) { ltreeEntity.LTreeAsString = ltreeEntity.LTree; ltreeEntity.SomeString = "*.Astrophysics"; - ltreeEntity.LTrees = new LTree[] { ltreeEntity.LTree, "Foo" }; + ltreeEntity.LTrees = [ltreeEntity.LTree, "Foo"]; } context.LTreeEntities.AddRange(ltreeEntities); - context.SaveChanges(); + await context.SaveChangesAsync(); } } @@ -565,8 +560,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(LTreeQueryContext context) - => LTreeQueryContext.Seed(context); + protected override Task SeedAsync(LTreeQueryContext context) + => LTreeQueryContext.SeedAsync(context); } #endregion diff --git a/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs new file mode 100644 index 000000000..e2a5bb1f0 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs @@ -0,0 +1,104 @@ +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 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 cb13429ec..3e0201d9a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs @@ -79,14 +79,9 @@ private TimestampQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class TimestampQueryContext : PoolableDbContext + public class TimestampQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet Entities { get; set; } - - public TimestampQueryContext(DbContextOptions options) - : base(options) - { - } } public class Entity @@ -116,10 +111,13 @@ public LegacyTimestampQueryFixture() NpgsqlTypeMappingSource.LegacyTimestampBehavior = true; } - public override void Dispose() - => NpgsqlTypeMappingSource.LegacyTimestampBehavior = false; + public override Task DisposeAsync() + { + NpgsqlTypeMappingSource.LegacyTimestampBehavior = false; + return Task.CompletedTask; + } - protected override void Seed(TimestampQueryContext context) + protected override async Task SeedAsync(TimestampQueryContext context) { using var ctx = CreateContext(); @@ -141,7 +139,8 @@ protected override void Seed(TimestampQueryContext context) TimestampDateTime = DateTime.SpecifyKind(utcDateTime2.ToLocalTime(), DateTimeKind.Unspecified), TimestampDateTimeOffset = new DateTimeOffset(utcDateTime2) }); - ctx.SaveChanges(); + + await ctx.SaveChangesAsync(); } } @@ -149,9 +148,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/MathQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/MathQueryTest.cs deleted file mode 100644 index c45f7e47b..000000000 --- a/test/EFCore.PG.FunctionalTests/Query/MathQueryTest.cs +++ /dev/null @@ -1,367 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; - -/// -/// Provides unit tests for PostgreSQL-specific math functions. -/// -/// -/// See: -/// - https://www.postgresql.org/docs/current/static/functions-math.html -/// - https://www.postgresql.org/docs/current/static/functions-conditional.html#FUNCTIONS-GREATEST-LEAST -/// -public class MathQueryTest : IClassFixture -{ - private MathQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public MathQueryTest(MathQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region GREATEST - - //[Fact] - public void Max_ushort_ushort() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.UShort, x.UShort)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"UShort\", x.\"UShort\")"); - } - - //[Fact] - public void Max_uint_uint() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.UInt, x.UInt)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"UInt\", x.\"UInt\")"); - } - - //[Fact] - public void Max_ulong_ulong() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.ULong, x.ULong)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"ULong\", x.\"ULong\")"); - } - - [Fact] - public void Max_short_short() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Short, x.Short)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Short\", x.\"Short\")"); - } - - [Fact] - public void Max_int_int() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Int, x.Int)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Int\", x.\"Int\")"); - } - - [Fact] - public void Max_long_long() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Long, x.Long)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Long\", x.\"Long\")"); - } - - [Fact] - public void Max_float_float() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Float, x.Float)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Float\", x.\"Float\")"); - } - - [Fact] - public void Max_double_double() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Double, x.Double)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Double\", x.\"Double\")"); - } - - [Fact] - public void Max_decimal_decimal() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Decimal, x.Decimal)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Decimal\", x.\"Decimal\")"); - } - - //[Fact] - public void Max_sbyte_sbyte() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.SByte, x.SByte)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"SByte\", x.\"SByte\")"); - } - - //[Fact] - public void Max_byte_byte() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Max(x.Byte, x.Byte)) - .ToArray(); - - AssertContainsSql("SELECT GREATEST(x.\"Byte\", x.\"Byte\")"); - } - - #endregion - - #region Least - - //[Fact] - public void Min_ushort_ushort() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.UShort, x.UShort)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"UShort\", x.\"UShort\")"); - } - - //[Fact] - public void Min_uint_uint() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.UInt, x.UInt)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"UInt\", x.\"UInt\")"); - } - - //[Fact] - public void Min_ulong_ulong() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.ULong, x.ULong)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"ULong\", x.\"ULong\")"); - } - - [Fact] - public void Min_short_short() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Short, x.Short)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Short\", x.\"Short\")"); - } - - [Fact] - public void Min_int_int() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Int, x.Int)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Int\", x.\"Int\")"); - } - - [Fact] - public void Min_long_long() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Long, x.Long)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Long\", x.\"Long\")"); - } - - [Fact] - public void Min_float_float() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Float, x.Float)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Float\", x.\"Float\")"); - } - - [Fact] - public void Min_double_double() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Double, x.Double)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Double\", x.\"Double\")"); - } - - [Fact] - public void Min_decimal_decimal() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Decimal, x.Decimal)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Decimal\", x.\"Decimal\")"); - } - - //[Fact] - public void Min_sbyte_sbyte() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.SByte, x.SByte)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"SByte\", x.\"SByte\")"); - } - - //[Fact] - public void Min_byte_byte() - { - using var ctx = CreateContext(); - var _ = - ctx.MathTestEntities - .Select(x => Math.Min(x.Byte, x.Byte)) - .ToArray(); - - AssertContainsSql("SELECT LEAST(x.\"Byte\", x.\"Byte\")"); - } - - #endregion - - #region Fixtures - - /// - /// Represents a fixture suitable for testing GREATEST(...) and LEAST(...)/ - /// - public class MathQueryNpgsqlFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "MathQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - } - - /// - /// Represents an entity suitable for testing GREATEST(...) and LEAST(...) operators. - /// - public class MathTestEntity - { - public int Id { get; set; } - public ushort UShort { get; set; } - public uint UInt { get; set; } - public ulong ULong { get; set; } - public short Short { get; set; } - public int Int { get; set; } - public long Long { get; set; } - public float Float { get; set; } - public double Double { get; set; } - public decimal Decimal { get; set; } - public byte Byte { get; set; } - public sbyte SByte { get; set; } - } - - /// - /// Represents a database suitable for testing GREATEST(...) and LEAST(...). - /// - public class MathContext : PoolableDbContext - { - /// - /// Represents a set of entities with numeric properties. - /// - public DbSet MathTestEntities { get; set; } - - /// - public MathContext(DbContextOptions options) - : base(options) - { - } - } - - #endregion - - #region Helpers - - protected MathContext CreateContext() - => Fixture.CreateContext(); - - /// - /// Asserts that the SQL fragment appears in the logs. - /// - /// The SQL statement or fragment to search for in the logs. - public void AssertContainsSql(string sql) - => Assert.Contains( - sql.Replace(Environment.NewLine, " ").Remove('\r').Remove('\n'), - Fixture.TestSqlLoggerFactory.Sql.Replace(Environment.NewLine, " ").Remove('\r').Remove('\n')); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs index 0c6ad1b1c..5f896f298 100644 --- a/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs @@ -488,7 +488,7 @@ public void Intersect_aggregate() .Select(g => g.Select(x => x.IntMultirange).RangeIntersectAgg()) .Single(); - Assert.Equal(new NpgsqlRange[] { new(4, true, 6, false), new(7, true, 9, false) }, intersection); + Assert.Equal([new(4, true, 6, false), new(7, true, 9, false)], intersection); AssertSql( """ @@ -730,8 +730,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(MultirangeContext context) - => MultirangeContext.Seed(context); + protected override Task SeedAsync(MultirangeContext context) + => MultirangeContext.SeedAsync(context); } public class MultirangeTestEntity @@ -749,54 +749,47 @@ public class MultirangeTestEntity private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class MultirangeContext : PoolableDbContext + public class MultirangeContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet TestEntities { get; set; } - public MultirangeContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(MultirangeContext context) + public static async Task SeedAsync(MultirangeContext context) { context.TestEntities.AddRange( new MultirangeTestEntity { Id = 1, - IntMultirange = new NpgsqlRange[] { new(0, 5), new(7, 10) }, - LongMultirange = new NpgsqlRange[] { new(0, 5), new(7, 10) }, - DecimalMultirange = new NpgsqlRange[] { new(0, 5), new(7, 10) }, + IntMultirange = [new(0, 5), new(7, 10)], + LongMultirange = [new(0, 5), new(7, 10)], + DecimalMultirange = [new(0, 5), new(7, 10)], DateOnlyDateMultirange = - new NpgsqlRange[] - { - new(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 5)), + [ + new(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 5)), new(new DateOnly(2020, 1, 7), new DateOnly(2020, 1, 10)) - }, - DateTimeDateMultirange = new NpgsqlRange[] - { + ], + DateTimeDateMultirange = + [ new(new DateTime(2020, 1, 1), new DateTime(2020, 1, 5)), new(new DateTime(2020, 1, 7), new DateTime(2020, 1, 10)) - } + ] }, new MultirangeTestEntity { Id = 2, - IntMultirange = new NpgsqlRange[] { new(4, 8), new(13, 20) }, - LongMultirange = new NpgsqlRange[] { new(4, 8), new(13, 20) }, - DecimalMultirange = new NpgsqlRange[] { new(4, 8), new(13, 20) }, + IntMultirange = [new(4, 8), new(13, 20)], + LongMultirange = [new(4, 8), new(13, 20)], + DecimalMultirange = [new(4, 8), new(13, 20)], DateOnlyDateMultirange = - new NpgsqlRange[] - { - new(new DateOnly(2020, 1, 4), new DateOnly(2020, 1, 8)), + [ + new(new DateOnly(2020, 1, 4), new DateOnly(2020, 1, 8)), new(new DateOnly(2020, 1, 13), new DateOnly(2020, 1, 20)) - }, - DateTimeDateMultirange = new NpgsqlRange[] - { + ], + DateTimeDateMultirange = + [ new(new DateTime(2020, 1, 4), new DateTime(2020, 1, 8)), new(new DateTime(2020, 1, 13), new DateTime(2020, 1, 20)) - } + ] }); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs b/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs index d5f412c76..bb9e565e5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs @@ -2,12 +2,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class NavigationTest : IClassFixture +public class NavigationTest(NavigationTestFixture fixture) : IClassFixture { [Fact] public void Duplicate_entries_are_not_created_for_navigations_to_principal() { - using var context = _fixture.CreateContext(); + using var context = fixture.CreateContext(); context.ConfigAction = modelBuilder => { @@ -31,7 +31,7 @@ public void Duplicate_entries_are_not_created_for_navigations_to_principal() [Fact] public void Duplicate_entries_are_not_created_for_navigations_to_dependant() { - using var context = _fixture.CreateContext(); + using var context = fixture.CreateContext(); context.ConfigAction = modelBuilder => { @@ -51,13 +51,6 @@ public void Duplicate_entries_are_not_created_for_navigations_to_dependant() "ForeignKey: GoTPerson {'SiblingReverseId'} -> GoTPerson {'Id'} ClientSetNull ToDependent: Siblings ToPrincipal: SiblingReverse", entityType.GetForeignKeys().Skip(1).First().ToString()); } - - private readonly NavigationTestFixture _fixture; - - public NavigationTest(NavigationTestFixture fixture) - { - _fixture = fixture; - } } public class GoTPerson @@ -71,13 +64,8 @@ public class GoTPerson public GoTPerson SiblingReverse { get; set; } } -public class GoTContext : DbContext +public class GoTContext(DbContextOptions options) : DbContext(options) { - public GoTContext(DbContextOptions options) - : base(options) - { - } - public DbSet People { get; set; } public Func ConfigAction { get; set; } diff --git a/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs index 0af672aef..3301d9e0b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs @@ -1458,8 +1458,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(NetContext context) - => NetContext.Seed(context); + protected override Task SeedAsync(NetContext context) + => NetContext.SeedAsync(context); } /// @@ -1537,7 +1537,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - public static void Seed(NetContext context) + public static async Task SeedAsync(NetContext context) { for (var i = 1; i <= 9; i++) { @@ -1557,7 +1557,7 @@ public static void Seed(NetContext context) }); } - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs new file mode 100644 index 000000000..1ed6bed17 --- /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 async Task SeedAsync(NodaTimeContext context) + { + context.AddRange(NodaTimeData.CreateNodaTimeTypes()); + await context.SaveChangesAsync(); + } + } + + 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 Task SeedAsync(NodaTimeContext context) + => NodaTimeContext.SeedAsync(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/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs index 90ef8b15c..b4ffa354e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs @@ -1,4 +1,4 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; @@ -57,11 +57,11 @@ public override async Task Multidimensional_array_is_not_supported() // supported). var contextFactory = await InitializeAsync( mb => mb.Entity().Property("MultidimensionalArray"), - seed: context => + seed: async context => { var entry = context.Add(new TestEntity()); entry.Property("MultidimensionalArray").CurrentValue = new[,] { { 1, 2 }, { 3, 4 } }; - context.SaveChanges(); + await context.SaveChangesAsync(); }); await using var context = contextFactory.CreateContext(); @@ -77,9 +77,36 @@ await Assert.ThrowsAsync( public override async Task Column_collection_inside_json_owned_entity() { - var exception = await Assert.ThrowsAsync(() => base.Column_collection_inside_json_owned_entity()); + await base.Column_collection_inside_json_owned_entity(); + + AssertSql( + """ +SELECT t."Id", t."Owned" +FROM "TestOwner" AS t +WHERE cardinality((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(t."Owned" -> 'Strings') WITH ORDINALITY AS t(element) ORDER BY ordinality))) = 2 +LIMIT 2 +""", + // + """ +SELECT t."Id", t."Owned" +FROM "TestOwner" AS t +WHERE ((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(t."Owned" -> 'Strings') WITH ORDINALITY AS t(element) ORDER BY ordinality)))[2] = 'bar' +LIMIT 2 +"""); + } + + protected override DbContextOptionsBuilder SetTranslateParameterizedCollectionsToConstants(DbContextOptionsBuilder optionsBuilder) + { + new NpgsqlDbContextOptionsBuilder(optionsBuilder).TranslateParameterizedCollectionsToConstants(); + + return optionsBuilder; + } + + protected override DbContextOptionsBuilder SetTranslateParameterizedCollectionsToParameters(DbContextOptionsBuilder optionsBuilder) + { + new NpgsqlDbContextOptionsBuilder(optionsBuilder).TranslateParameterizedCollectionsToParameters(); - Assert.Equal(exception.Message, NpgsqlStrings.Ef7JsonMappingNotSupported); + return optionsBuilder; } protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs index e5dc276ad..fd286401b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs @@ -16,14 +16,33 @@ public NorthwindAggregateOperatorsQueryNpgsqlTest( } // Overriding to add equality tolerance because of floating point precision - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public override Task Average_over_max_subquery_is_client_eval(bool async) - => AssertAverage( + public override async Task Average_over_max_subquery(bool async) + { + await AssertAverage( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), selector: c => (decimal)c.Orders.Average(o => 5 + o.OrderDetails.Max(od => od.ProductID)), - asserter: (a, b) => Assert.Equal(a, b, 15)); + asserter: (e, a) => Assert.Equal(e, a, 10)); + + AssertSql( + """ +@__p_0='3' + +SELECT avg(( + SELECT avg(CAST(5 + ( + SELECT max(o0."ProductID") + FROM "Order Details" AS o0 + WHERE o."OrderID" = o0."OrderID") AS double precision)) + FROM "Orders" AS o + WHERE c0."CustomerID" = o."CustomerID")::numeric) +FROM ( + SELECT c."CustomerID" + FROM "Customers" AS c + ORDER BY c."CustomerID" NULLS FIRST + LIMIT @__p_0 +) AS c0 +"""); + } public override async Task Contains_with_local_uint_array_closure(bool async) { @@ -91,12 +110,24 @@ await Assert.ThrowsAsync( public override async Task Contains_with_local_enumerable_inline_closure_mix(bool async) { - // Issue #31776 - await Assert.ThrowsAsync( - async () => - await base.Contains_with_local_enumerable_inline_closure_mix(async)); + await base.Contains_with_local_enumerable_inline_closure_mix(async); - AssertSql(); + AssertSql( + """ +@__p_0={ 'ABCDE', 'ALFKI' } (DbType = Object) + +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."CustomerID" = ANY (array_remove(@__p_0, NULL)) +""", + // + """ +@__p_0={ 'ABCDE', 'ANATR' } (DbType = Object) + +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."CustomerID" = ANY (array_remove(@__p_0, NULL)) +"""); } public override async Task Contains_with_local_non_primitive_list_closure_mix(bool async) diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs index 57e6c9f71..425b5782e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs @@ -106,6 +106,21 @@ WHERE c."ContactName" ILIKE '!%' ESCAPE '!' """); } + [Fact] + public void String_ILike_negated() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => !EF.Functions.ILike(c.ContactName, "%M%")); + + Assert.Equal(57, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" NOT ILIKE '%M%' OR c."ContactName" IS NULL +"""); + } + #endregion #region Collation @@ -212,6 +227,34 @@ WHERE string_to_array(c."ContactName", ' ', 'Maria') = ARRAY[NULL,'Anders']::tex """); } + [Fact] + public void ToDate() + { + using var context = CreateContext(); + var count = context.Orders.Count(c => EF.Functions.ToDate(c.OrderDate.ToString(), "YYYY-MM-DD") < new DateOnly(2000, 01, 01)); + Assert.Equal(830, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +WHERE to_date(COALESCE(o."OrderDate"::text, ''), 'YYYY-MM-DD') < DATE '2000-01-01' +"""); + } + + [Fact] + public void ToTimestamp() + { + using var context = CreateContext(); + var count = context.Orders.Count(c => EF.Functions.ToTimestamp(c.OrderDate.ToString(), "YYYY-MM-DD") < new DateTime(2000, 01, 01, 0,0,0, DateTimeKind.Utc)); + Assert.Equal(830, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +WHERE to_timestamp(COALESCE(o."OrderDate"::text, ''), 'YYYY-MM-DD') < TIMESTAMPTZ '2000-01-01T00:00:00Z' +"""); + } + #endregion private void AssertSql(params string[] expected) diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs index f74474e82..43c78f8c2 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs @@ -47,24 +47,17 @@ public override Task Convert_ToString(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task String_Join_non_aggregate(bool async) + public override async Task String_Join_non_aggregate(bool async) { - var param = "param"; - string nullParam = null; - - await AssertQuery( - async, - ss => ss.Set().Where( - c => string.Join("|", c.CustomerID, c.CompanyName, param, nullParam, "constant", null) - == "ALFKI|Alfreds Futterkiste|param||constant|")); + await base.String_Join_non_aggregate(async); AssertSql( """ -@__param_0='param' +@__foo_0='foo' SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c -WHERE concat_ws('|', c."CustomerID", c."CompanyName", COALESCE(@__param_0, ''), COALESCE(NULL, ''), 'constant', '') = 'ALFKI|Alfreds Futterkiste|param||constant|' +WHERE concat_ws('|', c."CompanyName", @__foo_0, '', 'bar') = 'Around the Horn|foo||bar' """); } @@ -118,6 +111,23 @@ await AssertQuery( """); } + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_constant_pattern_properly_escaped(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A';foo")), + assertEmpty: true); + + AssertSql( + """ +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."CompanyName" ~ '(?p)^A'';foo' +"""); + } + [Theory] [MemberData(nameof(IsAsyncData))] public async Task Regex_IsMatch_with_parameter_pattern(bool async) @@ -138,6 +148,22 @@ await AssertQuery( """); } + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_negated(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => !Regex.IsMatch(c.CompanyName, "^A"))); + + AssertSql( + """ +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."CompanyName" !~ '(?p)^A' +"""); + } + [Theory] [MemberData(nameof(IsAsyncData))] public async Task Regex_IsMatchOptionsNone(bool async) @@ -170,6 +196,22 @@ await AssertQuery( """); } + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_IgnoreCase_negated(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => !Regex.IsMatch(c.CompanyName, "^a", RegexOptions.IgnoreCase))); + + AssertSql( + """ +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."CompanyName" !~* '(?p)^a' +"""); + } + [Theory] [MemberData(nameof(IsAsyncData))] public async Task Regex_IsMatch_with_Multiline(bool async) @@ -541,7 +583,7 @@ public virtual async Task GroupBy_JsonObjectAgg(bool async) var london = results.Single(r => r.City == "London"); Assert.Equal( - @"{ ""Around the Horn"" : ""Thomas Hardy"", ""B's Beverages"" : ""Victoria Ashworth"", ""Consolidated Holdings"" : ""Elizabeth Brown"", ""Eastern Connection"" : ""Ann Devon"", ""North/South"" : ""Simon Crowther"", ""Seven Seas Imports"" : ""Hari Kumar"" }", + """{ "Around the Horn" : "Thomas Hardy", "B's Beverages" : "Victoria Ashworth", "Consolidated Holdings" : "Elizabeth Brown", "Eastern Connection" : "Ann Devon", "North/South" : "Simon Crowther", "Seven Seas Imports" : "Hari Kumar" }""", london.Companies); AssertSql( diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs index 52bf83e88..d413e5c8c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs @@ -252,12 +252,12 @@ public override async Task GroupBy_Property_Select_Key_with_constant(bool async) AssertSql( """ -SELECT t."Name", t."CustomerID" AS "Value", count(*)::int AS "Count" +SELECT o0."Name", o0."CustomerID" AS "Value", count(*)::int AS "Count" FROM ( SELECT o."CustomerID", 'CustomerID' AS "Name" FROM "Orders" AS o -) AS t -GROUP BY t."Name", t."CustomerID" +) AS o0 +GROUP BY o0."Name", o0."CustomerID" """); } @@ -636,12 +636,12 @@ public override async Task GroupBy_Constant_Select_Sum_Min_Key_Max_Avg(bool asyn AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", min(t."OrderID") AS "Min", t."Key", max(t."OrderID") AS "Max", avg(t."OrderID"::double precision) AS "Avg" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", min(o0."OrderID") AS "Min", o0."Key", max(o0."OrderID") AS "Max", avg(o0."OrderID"::double precision) AS "Avg" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -651,12 +651,12 @@ public override async Task GroupBy_Constant_with_element_selector_Select_Sum(boo AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -666,12 +666,12 @@ public override async Task GroupBy_Constant_with_element_selector_Select_Sum2(bo AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -681,12 +681,12 @@ public override async Task GroupBy_Constant_with_element_selector_Select_Sum3(bo AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -696,13 +696,13 @@ public override async Task GroupBy_after_predicate_Constant_Select_Sum_Min_Key_M AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", min(t."OrderID") AS "Min", t."Key" AS "Random", max(t."OrderID") AS "Max", avg(t."OrderID"::double precision) AS "Avg" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", min(o0."OrderID") AS "Min", o0."Key" AS "Random", max(o0."OrderID") AS "Max", avg(o0."OrderID"::double precision) AS "Avg" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o WHERE o."OrderID" > 10500 -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -712,12 +712,12 @@ public override async Task GroupBy_Constant_with_element_selector_Select_Sum_Min AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", t."Key" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" FROM ( SELECT o."OrderID", 2 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -727,13 +727,13 @@ public override async Task GroupBy_constant_with_where_on_grouping_with_aggregat AssertSql( """ -SELECT min(t."OrderDate") FILTER (WHERE 1 = t."Key") AS "Min", max(t."OrderDate") FILTER (WHERE 1 = t."Key") AS "Max", COALESCE(sum(t."OrderID") FILTER (WHERE 1 = t."Key"), 0)::int AS "Sum", avg(t."OrderID"::double precision) FILTER (WHERE 1 = t."Key") AS "Average" +SELECT min(o0."OrderDate") FILTER (WHERE 1 = o0."Key") AS "Min", max(o0."OrderDate") FILTER (WHERE 1 = o0."Key") AS "Max", COALESCE(sum(o0."OrderID") FILTER (WHERE 1 = o0."Key"), 0)::int AS "Sum", avg(o0."OrderID"::double precision) FILTER (WHERE 1 = o0."Key") AS "Average" FROM ( SELECT o."OrderID", o."OrderDate", 1 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" -ORDER BY t."Key" NULLS FIRST +) AS o0 +GROUP BY o0."Key" +ORDER BY o0."Key" NULLS FIRST """); } @@ -745,12 +745,12 @@ public override async Task GroupBy_param_Select_Sum_Min_Key_Max_Avg(bool async) """ @__a_0='2' -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", min(t."OrderID") AS "Min", t."Key", max(t."OrderID") AS "Max", avg(t."OrderID"::double precision) AS "Avg" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", min(o0."OrderID") AS "Min", o0."Key", max(o0."OrderID") AS "Max", avg(o0."OrderID"::double precision) AS "Avg" FROM ( SELECT o."OrderID", @__a_0 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -762,12 +762,12 @@ public override async Task GroupBy_param_with_element_selector_Select_Sum(bool a """ @__a_0='2' -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", @__a_0 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -779,12 +779,12 @@ public override async Task GroupBy_param_with_element_selector_Select_Sum2(bool """ @__a_0='2' -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", @__a_0 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -796,12 +796,12 @@ public override async Task GroupBy_param_with_element_selector_Select_Sum3(bool """ @__a_0='2' -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", @__a_0 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -813,12 +813,12 @@ public override async Task GroupBy_param_with_element_selector_Select_Sum_Min_Ke """ @__a_0='2' -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", t."Key" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" FROM ( SELECT o."OrderID", @__a_0 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -828,13 +828,13 @@ public override async Task GroupBy_anonymous_key_type_mismatch_with_aggregate(bo AssertSql( """ -SELECT count(*)::int AS "I0", t."I0" AS "I1" +SELECT count(*)::int AS "I0", o0."I0" AS "I1" FROM ( SELECT date_part('year', o."OrderDate")::int AS "I0" FROM "Orders" AS o -) AS t -GROUP BY t."I0" -ORDER BY t."I0" NULLS FIRST +) AS o0 +GROUP BY o0."I0" +ORDER BY o0."I0" NULLS FIRST """); } @@ -1081,12 +1081,12 @@ public override async Task GroupBy_conditional_properties(bool async) AssertSql( """ -SELECT t."OrderMonth", t."CustomerID" AS "Customer", count(*)::int AS "Count" +SELECT o0."OrderMonth", o0."CustomerID" AS "Customer", count(*)::int AS "Count" FROM ( SELECT o."CustomerID", NULL AS "OrderMonth" FROM "Orders" AS o -) AS t -GROUP BY t."OrderMonth", t."CustomerID" +) AS o0 +GROUP BY o0."OrderMonth", o0."CustomerID" """); } @@ -1096,12 +1096,12 @@ public override async Task GroupBy_empty_key_Aggregate(bool async) AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int +SELECT COALESCE(sum(o0."OrderID"), 0)::int FROM ( SELECT o."OrderID", 1 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -1111,12 +1111,12 @@ public override async Task GroupBy_empty_key_Aggregate_Key(bool async) AssertSql( """ -SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum" +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" FROM ( SELECT o."OrderID", 1 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -1140,14 +1140,14 @@ public override async Task OrderBy_Skip_GroupBy_Aggregate(bool async) """ @__p_0='80' -SELECT avg(t."OrderID"::double precision) +SELECT avg(o0."OrderID"::double precision) FROM ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o ORDER BY o."OrderID" NULLS FIRST OFFSET @__p_0 -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -1159,14 +1159,14 @@ public override async Task OrderBy_Take_GroupBy_Aggregate(bool async) """ @__p_0='500' -SELECT min(t."OrderID") +SELECT min(o0."OrderID") FROM ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o ORDER BY o."OrderID" NULLS FIRST LIMIT @__p_0 -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -1179,14 +1179,14 @@ public override async Task OrderBy_Skip_Take_GroupBy_Aggregate(bool async) @__p_1='500' @__p_0='80' -SELECT max(t."OrderID") +SELECT max(o0."OrderID") FROM ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o ORDER BY o."OrderID" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -1196,12 +1196,12 @@ public override async Task Distinct_GroupBy_Aggregate(bool async) AssertSql( """ -SELECT t."CustomerID" AS "Key", count(*)::int AS c +SELECT o0."CustomerID" AS "Key", count(*)::int AS c FROM ( SELECT DISTINCT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -1211,12 +1211,12 @@ public override async Task Anonymous_projection_Distinct_GroupBy_Aggregate(bool AssertSql( """ -SELECT t."EmployeeID" AS "Key", count(*)::int AS c +SELECT o0."EmployeeID" AS "Key", count(*)::int AS c FROM ( SELECT DISTINCT o."OrderID", o."EmployeeID" FROM "Orders" AS o -) AS t -GROUP BY t."EmployeeID" +) AS o0 +GROUP BY o0."EmployeeID" """); } @@ -1269,22 +1269,22 @@ public override async Task Join_complex_GroupBy_Aggregate(bool async) @__p_2='50' @__p_1='10' -SELECT t0."CustomerID" AS "Key", avg(t."OrderID"::double precision) AS "Count" +SELECT c0."CustomerID" AS "Key", avg(o0."OrderID"::double precision) AS "Count" FROM ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10400 ORDER BY o."OrderDate" NULLS FIRST LIMIT @__p_0 -) AS t +) AS o0 INNER JOIN ( SELECT c."CustomerID" FROM "Customers" AS c WHERE c."CustomerID" NOT IN ('DRACD', 'FOLKO') ORDER BY c."City" NULLS FIRST LIMIT @__p_2 OFFSET @__p_1 -) AS t0 ON t."CustomerID" = t0."CustomerID" -GROUP BY t0."CustomerID" +) AS c0 ON o0."CustomerID" = c0."CustomerID" +GROUP BY c0."CustomerID" """); } @@ -1377,23 +1377,23 @@ public override async Task GroupJoin_complex_GroupBy_Aggregate(bool async) @__p_0='10' @__p_2='100' -SELECT t0."CustomerID" AS "Key", avg(t0."OrderID"::double precision) AS "Count" +SELECT o0."CustomerID" AS "Key", avg(o0."OrderID"::double precision) AS "Count" FROM ( SELECT c."CustomerID" FROM "Customers" AS c WHERE c."CustomerID" NOT IN ('DRACD', 'FOLKO') ORDER BY c."City" NULLS FIRST LIMIT @__p_1 OFFSET @__p_0 -) AS t +) AS c0 INNER JOIN ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10400 ORDER BY o."OrderDate" NULLS FIRST LIMIT @__p_2 -) AS t0 ON t."CustomerID" = t0."CustomerID" -WHERE t0."OrderID" > 10300 -GROUP BY t0."CustomerID" +) AS o0 ON c0."CustomerID" = o0."CustomerID" +WHERE o0."OrderID" > 10300 +GROUP BY o0."CustomerID" """); } @@ -1431,7 +1431,7 @@ public override async Task Union_simple_groupby(bool async) AssertSql( """ -SELECT t."City" AS "Key", count(*)::int AS "Total" +SELECT u."City" AS "Key", count(*)::int AS "Total" FROM ( SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c @@ -1440,8 +1440,8 @@ public override async Task Union_simple_groupby(bool async) SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM "Customers" AS c0 WHERE c0."City" = 'México D.F.' -) AS t -GROUP BY t."City" +) AS u +GROUP BY u."City" """); } @@ -1477,12 +1477,12 @@ public override async Task GroupBy_after_anonymous_projection_and_distinct_follo AssertSql( """ -SELECT t."CustomerID" AS "Key", count(*)::int AS "Count" +SELECT o0."CustomerID" AS "Key", count(*)::int AS "Count" FROM ( SELECT DISTINCT o."CustomerID", o."OrderID" FROM "Orders" AS o -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -1492,13 +1492,13 @@ public override async Task GroupBy_complex_key_aggregate(bool async) AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT s."Key", count(*)::int AS "Count" FROM ( SELECT substring(c."CustomerID", 1, 1) AS "Key" FROM "Orders" AS o LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -) AS t -GROUP BY t."Key" +) AS s +GROUP BY s."Key" """); } @@ -1508,15 +1508,15 @@ public override async Task GroupBy_complex_key_aggregate_2(bool async) AssertSql( """ -SELECT t."Key" AS "Month", COALESCE(sum(t."OrderID"), 0)::int AS "Total", ( - SELECT COALESCE(sum(o0."OrderID"), 0)::int - FROM "Orders" AS o0 - WHERE date_part('month', o0."OrderDate")::int = t."Key" OR (o0."OrderDate" IS NULL AND t."Key" IS NULL)) AS "Payment" +SELECT o0."Key" AS "Month", COALESCE(sum(o0."OrderID"), 0)::int AS "Total", ( + SELECT COALESCE(sum(o1."OrderID"), 0)::int + FROM "Orders" AS o1 + WHERE date_part('month', o1."OrderDate")::int = o0."Key" OR (o1."OrderDate" IS NULL AND o0."Key" IS NULL)) AS "Payment" FROM ( SELECT o."OrderID", date_part('month', o."OrderDate")::int AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -1596,7 +1596,7 @@ public override async Task GroupBy_aggregate_Pushdown(bool async) @__p_0='20' @__p_1='4' -SELECT t."CustomerID" +SELECT o0."CustomerID" FROM ( SELECT o."CustomerID" FROM "Orders" AS o @@ -1604,8 +1604,8 @@ GROUP BY o."CustomerID" HAVING count(*)::int > 10 ORDER BY o."CustomerID" NULLS FIRST LIMIT @__p_0 -) AS t -ORDER BY t."CustomerID" NULLS FIRST +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST OFFSET @__p_1 """); } @@ -1619,7 +1619,7 @@ public override async Task GroupBy_aggregate_using_grouping_key_Pushdown(bool as @__p_0='20' @__p_1='4' -SELECT t."Key", t."Max" +SELECT o0."Key", o0."Max" FROM ( SELECT o."CustomerID" AS "Key", max(o."CustomerID") AS "Max" FROM "Orders" AS o @@ -1627,8 +1627,8 @@ GROUP BY o."CustomerID" HAVING count(*)::int > 10 ORDER BY o."CustomerID" NULLS FIRST LIMIT @__p_0 -) AS t -ORDER BY t."Key" NULLS FIRST +) AS o0 +ORDER BY o0."Key" NULLS FIRST OFFSET @__p_1 """); } @@ -1642,7 +1642,7 @@ public override async Task GroupBy_aggregate_Pushdown_followed_by_projecting_Len @__p_0='20' @__p_1='4' -SELECT length(t."CustomerID")::int +SELECT length(o0."CustomerID")::int FROM ( SELECT o."CustomerID" FROM "Orders" AS o @@ -1650,8 +1650,8 @@ GROUP BY o."CustomerID" HAVING count(*)::int > 10 ORDER BY o."CustomerID" NULLS FIRST LIMIT @__p_0 -) AS t -ORDER BY t."CustomerID" NULLS FIRST +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST OFFSET @__p_1 """); } @@ -1673,8 +1673,8 @@ GROUP BY o."CustomerID" HAVING count(*)::int > 10 ORDER BY o."CustomerID" NULLS FIRST LIMIT @__p_0 -) AS t -ORDER BY t."CustomerID" NULLS FIRST +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST OFFSET @__p_1 """); } @@ -1711,12 +1711,12 @@ public override async Task GroupBy_count_filter(bool async) AssertSql( """ -SELECT t."Key" AS "Name", count(*)::int AS "Count" +SELECT o0."Key" AS "Name", count(*)::int AS "Count" FROM ( SELECT 'Order' AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" HAVING count(*)::int > 0 """); } @@ -1741,15 +1741,15 @@ public override async Task GroupBy_Aggregate_Join(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" FROM ( SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t -INNER JOIN "Customers" AS c ON t."CustomerID" = c."CustomerID" -INNER JOIN "Orders" AS o0 ON t."LastOrderID" = o0."OrderID" +) AS o0 +INNER JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" """); } @@ -1762,11 +1762,11 @@ public override async Task GroupBy_Aggregate_Join_converted_from_SelectMany(bool SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c INNER JOIN ( - SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" + SELECT o."CustomerID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" """); } @@ -1779,11 +1779,11 @@ public override async Task GroupBy_Aggregate_LeftJoin_converted_from_SelectMany( SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c LEFT JOIN ( - SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" + SELECT o."CustomerID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" """); } @@ -1793,15 +1793,15 @@ public override async Task Join_GroupBy_Aggregate_multijoins(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" FROM "Customers" AS c INNER JOIN ( SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" -INNER JOIN "Orders" AS o0 ON t."LastOrderID" = o0."OrderID" +) AS o0 ON c."CustomerID" = o0."CustomerID" +INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" """); } @@ -1811,14 +1811,14 @@ public override async Task Join_GroupBy_Aggregate_single_join(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."LastOrderID" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."LastOrderID" FROM "Customers" AS c INNER JOIN ( SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" """); } @@ -1828,15 +1828,15 @@ public override async Task Join_GroupBy_Aggregate_with_another_join(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."LastOrderID", o0."OrderID" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."LastOrderID", o1."OrderID" FROM "Customers" AS c INNER JOIN ( SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" -INNER JOIN "Orders" AS o0 ON c."CustomerID" = o0."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" +INNER JOIN "Orders" AS o1 ON c."CustomerID" = o1."CustomerID" """); } @@ -1846,17 +1846,17 @@ public override async Task Join_GroupBy_Aggregate_distinct_single_join(bool asyn AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t0."LastOrderID" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o1."LastOrderID" FROM "Customers" AS c INNER JOIN ( - SELECT DISTINCT t."CustomerID", max(t."OrderID") AS "LastOrderID" + SELECT DISTINCT o0."CustomerID", max(o0."OrderID") AS "LastOrderID" FROM ( SELECT o."OrderID", o."CustomerID", date_part('year', o."OrderDate")::int AS "Year" FROM "Orders" AS o - ) AS t - GROUP BY t."CustomerID", t."Year" + ) AS o0 + GROUP BY o0."CustomerID", o0."Year" HAVING count(*)::int > 5 -) AS t0 ON c."CustomerID" = t0."CustomerID" +) AS o1 ON c."CustomerID" = o1."CustomerID" """); } @@ -1866,14 +1866,14 @@ public override async Task Join_GroupBy_Aggregate_with_left_join(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."LastOrderID" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."LastOrderID" FROM "Customers" AS c LEFT JOIN ( SELECT o."CustomerID", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" WHERE c."CustomerID" LIKE 'A%' """); } @@ -1884,18 +1884,18 @@ public override async Task Join_GroupBy_Aggregate_in_subquery(bool async) AssertSql( """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate", t0."CustomerID", t0."Address", t0."City", t0."CompanyName", t0."ContactName", t0."ContactTitle", t0."Country", t0."Fax", t0."Phone", t0."PostalCode", t0."Region" +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate", s."CustomerID", s."Address", s."City", s."CompanyName", s."ContactName", s."ContactTitle", s."Country", s."Fax", s."Phone", s."PostalCode", s."Region" FROM "Orders" AS o INNER JOIN ( SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c INNER JOIN ( - SELECT o0."CustomerID", max(o0."OrderID") AS "LastOrderID" + SELECT o0."CustomerID" FROM "Orders" AS o0 GROUP BY o0."CustomerID" HAVING count(*)::int > 5 - ) AS t ON c."CustomerID" = t."CustomerID" -) AS t0 ON o."CustomerID" = t0."CustomerID" + ) AS o1 ON c."CustomerID" = o1."CustomerID" +) AS s ON o."CustomerID" = s."CustomerID" WHERE o."OrderID" < 10400 """); } @@ -1906,14 +1906,14 @@ public override async Task Join_GroupBy_Aggregate_on_key(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."LastOrderID" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."LastOrderID" FROM "Customers" AS c INNER JOIN ( SELECT o."CustomerID" AS "Key", max(o."OrderID") AS "LastOrderID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 5 -) AS t ON c."CustomerID" = t."Key" +) AS o0 ON c."CustomerID" = o0."Key" """); } @@ -1959,13 +1959,13 @@ public override async Task Distinct_GroupBy_OrderBy_key(bool async) AssertSql( """ -SELECT t."CustomerID" AS "Key", count(*)::int AS c +SELECT o0."CustomerID" AS "Key", count(*)::int AS c FROM ( SELECT DISTINCT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o -) AS t -GROUP BY t."CustomerID" -ORDER BY t."CustomerID" NULLS FIRST +) AS o0 +GROUP BY o0."CustomerID" +ORDER BY o0."CustomerID" NULLS FIRST """); } @@ -1998,13 +1998,13 @@ public override async Task Select_uncorrelated_collection_with_groupby_works(boo AssertSql( """ -SELECT c."CustomerID", t."OrderID" +SELECT c."CustomerID", o0."OrderID" FROM "Customers" AS c LEFT JOIN LATERAL ( SELECT o."OrderID" FROM "Orders" AS o GROUP BY o."OrderID" -) AS t ON TRUE +) AS o0 ON TRUE WHERE c."CustomerID" LIKE 'A%' ORDER BY c."CustomerID" NULLS FIRST """); @@ -2016,20 +2016,20 @@ public override async Task Select_uncorrelated_collection_with_groupby_multiple_ AssertSql( """ -SELECT o."OrderID", t."ProductID", t0.c, t0."ProductID" +SELECT o."OrderID", p1."ProductID", p2.c, p2."ProductID" FROM "Orders" AS o LEFT JOIN LATERAL ( SELECT p."ProductID" FROM "Products" AS p GROUP BY p."ProductID" -) AS t ON TRUE +) AS p1 ON TRUE LEFT JOIN LATERAL ( SELECT count(*)::int AS c, p0."ProductID" FROM "Products" AS p0 GROUP BY p0."ProductID" -) AS t0 ON TRUE +) AS p2 ON TRUE WHERE o."CustomerID" LIKE 'A%' -ORDER BY o."OrderID" NULLS FIRST, t."ProductID" NULLS FIRST +ORDER BY o."OrderID" NULLS FIRST, p1."ProductID" NULLS FIRST """); } @@ -2293,22 +2293,22 @@ public override async Task GroupBy_Shadow2(bool async) AssertSql( """ -SELECT t0."EmployeeID", t0."City", t0."Country", t0."FirstName", t0."ReportsTo", t0."Title" +SELECT e3."EmployeeID", e3."City", e3."Country", e3."FirstName", e3."ReportsTo", e3."Title" FROM ( SELECT e."Title" FROM "Employees" AS e WHERE e."Title" = 'Sales Representative' AND e."EmployeeID" = 1 GROUP BY e."Title" -) AS t +) AS e1 LEFT JOIN ( - SELECT t1."EmployeeID", t1."City", t1."Country", t1."FirstName", t1."ReportsTo", t1."Title" + SELECT e2."EmployeeID", e2."City", e2."Country", e2."FirstName", e2."ReportsTo", e2."Title" FROM ( SELECT e0."EmployeeID", e0."City", e0."Country", e0."FirstName", e0."ReportsTo", e0."Title", ROW_NUMBER() OVER(PARTITION BY e0."Title" ORDER BY e0."EmployeeID" NULLS FIRST) AS row FROM "Employees" AS e0 WHERE e0."Title" = 'Sales Representative' AND e0."EmployeeID" = 1 - ) AS t1 - WHERE t1.row <= 1 -) AS t0 ON t."Title" = t0."Title" + ) AS e2 + WHERE e2.row <= 1 +) AS e3 ON e1."Title" = e3."Title" """); } @@ -2335,14 +2335,14 @@ public override async Task GroupBy_select_grouping_list(bool async) AssertSql( """ -SELECT t."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" +SELECT c1."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM ( SELECT c."City" FROM "Customers" AS c GROUP BY c."City" -) AS t -LEFT JOIN "Customers" AS c0 ON t."City" = c0."City" -ORDER BY t."City" NULLS FIRST +) AS c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST """); } @@ -2352,14 +2352,14 @@ public override async Task GroupBy_select_grouping_array(bool async) AssertSql( """ -SELECT t."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" +SELECT c1."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM ( SELECT c."City" FROM "Customers" AS c GROUP BY c."City" -) AS t -LEFT JOIN "Customers" AS c0 ON t."City" = c0."City" -ORDER BY t."City" NULLS FIRST +) AS c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST """); } @@ -2369,18 +2369,18 @@ public override async Task GroupBy_select_grouping_composed_list(bool async) AssertSql( """ -SELECT t."City", t0."CustomerID", t0."Address", t0."City", t0."CompanyName", t0."ContactName", t0."ContactTitle", t0."Country", t0."Fax", t0."Phone", t0."PostalCode", t0."Region" +SELECT c1."City", c2."CustomerID", c2."Address", c2."City", c2."CompanyName", c2."ContactName", c2."ContactTitle", c2."Country", c2."Fax", c2."Phone", c2."PostalCode", c2."Region" FROM ( SELECT c."City" FROM "Customers" AS c GROUP BY c."City" -) AS t +) AS c1 LEFT JOIN ( SELECT c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM "Customers" AS c0 WHERE c0."CustomerID" LIKE 'A%' -) AS t0 ON t."City" = t0."City" -ORDER BY t."City" NULLS FIRST +) AS c2 ON c1."City" = c2."City" +ORDER BY c1."City" NULLS FIRST """); } @@ -2390,14 +2390,14 @@ public override async Task GroupBy_select_grouping_composed_list_2(bool async) AssertSql( """ -SELECT t."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" +SELECT c1."City", c0."CustomerID", c0."Address", c0."City", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM ( SELECT c."City" FROM "Customers" AS c GROUP BY c."City" -) AS t -LEFT JOIN "Customers" AS c0 ON t."City" = c0."City" -ORDER BY t."City" NULLS FIRST, c0."CustomerID" NULLS FIRST +) AS c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST, c0."CustomerID" NULLS FIRST """); } @@ -2416,10 +2416,10 @@ public override async Task Count_after_GroupBy_aggregate(bool async) """ SELECT count(*)::int FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2431,10 +2431,10 @@ public override async Task LongCount_after_GroupBy_aggregate(bool async) """ SELECT count(*) FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2480,21 +2480,21 @@ public override async Task MinMax_after_GroupBy_aggregate(bool async) AssertSql( """ -SELECT min(t.c) +SELECT min(o0.c) FROM ( SELECT COALESCE(sum(o."OrderID"), 0)::int AS c FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """, // """ -SELECT max(t.c) +SELECT max(o0.c) FROM ( SELECT COALESCE(sum(o."OrderID"), 0)::int AS c FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2547,10 +2547,10 @@ public override async Task Count_after_GroupBy_without_aggregate(bool async) """ SELECT count(*)::int FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2562,11 +2562,11 @@ public override async Task Count_with_predicate_after_GroupBy_without_aggregate( """ SELECT count(*)::int FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 1 -) AS t +) AS o0 """); } @@ -2578,10 +2578,10 @@ public override async Task LongCount_after_GroupBy_without_aggregate(bool async) """ SELECT count(*) FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2593,11 +2593,11 @@ public override async Task LongCount_with_predicate_after_GroupBy_without_aggreg """ SELECT count(*) FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" HAVING count(*)::int > 1 -) AS t +) AS o0 """); } @@ -2648,16 +2648,16 @@ public override async Task GroupBy_aggregate_followed_by_another_GroupBy_aggrega AssertSql( """ -SELECT t0."Key0" AS "Key", COALESCE(sum(t0."Count"), 0)::int AS "Count" +SELECT o1."Key0" AS "Key", COALESCE(sum(o1."Count"), 0)::int AS "Count" FROM ( - SELECT t."Count", 1 AS "Key0" + SELECT o0."Count", 1 AS "Key0" FROM ( SELECT count(*)::int AS "Count" FROM "Orders" AS o GROUP BY o."CustomerID" - ) AS t -) AS t0 -GROUP BY t0."Key0" + ) AS o0 +) AS o1 +GROUP BY o1."Key0" """); } @@ -2670,15 +2670,18 @@ public override async Task GroupBy_Count_in_projection(bool async) SELECT o."OrderID", o."OrderDate", EXISTS ( SELECT 1 FROM "Order Details" AS o0 - WHERE o."OrderID" = o0."OrderID" AND o0."ProductID" < 25) AS "HasOrderDetails", ( - SELECT count(*)::int - FROM ( - SELECT p."ProductName" - FROM "Order Details" AS o1 - INNER JOIN "Products" AS p ON o1."ProductID" = p."ProductID" - WHERE o."OrderID" = o1."OrderID" AND o1."ProductID" < 25 - GROUP BY p."ProductName" - ) AS t) > 1 AS "HasMultipleProducts" + WHERE o."OrderID" = o0."OrderID" AND o0."ProductID" < 25) AS "HasOrderDetails", CASE + WHEN ( + SELECT count(*)::int + FROM ( + SELECT 1 + FROM "Order Details" AS o1 + INNER JOIN "Products" AS p ON o1."ProductID" = p."ProductID" + WHERE o."OrderID" = o1."OrderID" AND o1."ProductID" < 25 + GROUP BY p."ProductName" + ) AS s) > 1 THEN TRUE + ELSE FALSE +END AS "HasMultipleProducts" FROM "Orders" AS o WHERE o."OrderDate" IS NOT NULL """); @@ -2692,10 +2695,10 @@ public override async Task GroupBy_nominal_type_count(bool async) """ SELECT count(*)::int FROM ( - SELECT o."CustomerID" + SELECT 1 FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o0 """); } @@ -2717,12 +2720,12 @@ public override async Task GroupBy_based_on_renamed_property_complex(bool async) AssertSql( """ -SELECT t."Renamed" AS "Key", count(*)::int AS "Count" +SELECT c0."Renamed" AS "Key", count(*)::int AS "Count" FROM ( SELECT DISTINCT c."City" AS "Renamed", c."CustomerID" FROM "Customers" AS c -) AS t -GROUP BY t."Renamed" +) AS c0 +GROUP BY c0."Renamed" """); } @@ -2746,12 +2749,12 @@ public override async Task Odata_groupby_empty_key(bool async) AssertSql( """ -SELECT 'TotalAmount' AS "Name", COALESCE(sum(t."OrderID"::numeric), 0.0) AS "Value" +SELECT 'TotalAmount' AS "Name", COALESCE(sum(o0."OrderID"::numeric), 0.0) AS "Value" FROM ( SELECT o."OrderID", 1 AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -2837,14 +2840,14 @@ public override async Task GroupBy_with_order_by_skip_and_another_order_by(bool """ @__p_0='80' -SELECT COALESCE(sum(t."OrderID"), 0)::int +SELECT COALESCE(sum(o0."OrderID"), 0)::int FROM ( SELECT o."OrderID", o."CustomerID" FROM "Orders" AS o ORDER BY o."CustomerID" NULLS FIRST, o."OrderID" NULLS FIRST OFFSET @__p_0 -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -2941,13 +2944,13 @@ public override async Task GroupBy_aggregate_join_with_grouping_key(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."Count" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."Count" FROM ( SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t -INNER JOIN "Customers" AS c ON t."Key" = c."CustomerID" +) AS o0 +INNER JOIN "Customers" AS c ON o0."Key" = c."CustomerID" """); } @@ -2962,8 +2965,8 @@ public override async Task GroupBy_aggregate_join_with_group_result(bool async) SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "LastOrderDate" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t -INNER JOIN "Orders" AS o0 ON (t."Key" = o0."CustomerID" OR (t."Key" IS NULL AND o0."CustomerID" IS NULL)) AND (t."LastOrderDate" = o0."OrderDate" OR (t."LastOrderDate" IS NULL AND o0."OrderDate" IS NULL)) +) AS o1 +INNER JOIN "Orders" AS o0 ON (o1."Key" = o0."CustomerID" OR (o1."Key" IS NULL AND o0."CustomerID" IS NULL)) AND (o1."LastOrderDate" = o0."OrderDate" OR (o1."LastOrderDate" IS NULL AND o0."OrderDate" IS NULL)) """); } @@ -2975,14 +2978,14 @@ public override async Task GroupBy_aggregate_from_right_side_of_join(bool async) """ @__p_0='10' -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t."Max" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o0."Max" FROM "Customers" AS c INNER JOIN ( SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "Max" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t ON c."CustomerID" = t."Key" -ORDER BY t."Max" NULLS FIRST, c."CustomerID" NULLS FIRST +) AS o0 ON c."CustomerID" = o0."Key" +ORDER BY o0."Max" NULLS FIRST, c."CustomerID" NULLS FIRST LIMIT @__p_0 OFFSET @__p_0 """); } @@ -2993,18 +2996,18 @@ public override async Task GroupBy_aggregate_join_another_GroupBy_aggregate(bool AssertSql( """ -SELECT t."Key", t."Total", t0."ThatYear" +SELECT o1."Key", o1."Total", o2."ThatYear" FROM ( SELECT o."CustomerID" AS "Key", count(*)::int AS "Total" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o1 INNER JOIN ( SELECT o0."CustomerID" AS "Key", count(*)::int AS "ThatYear" FROM "Orders" AS o0 WHERE date_part('year', o0."OrderDate")::int = 1997 GROUP BY o0."CustomerID" -) AS t0 ON t."Key" = t0."Key" +) AS o2 ON o1."Key" = o2."Key" """); } @@ -3016,13 +3019,13 @@ public override async Task GroupBy_aggregate_after_skip_0_take_0(bool async) """ @__p_0='0' -SELECT t."CustomerID" AS "Key", count(*)::int AS "Total" +SELECT o0."CustomerID" AS "Key", count(*)::int AS "Total" FROM ( SELECT o."CustomerID" FROM "Orders" AS o LIMIT @__p_0 OFFSET @__p_0 -) AS t -GROUP BY t."CustomerID" +) AS o0 +GROUP BY o0."CustomerID" """); } @@ -3048,16 +3051,16 @@ public override async Task GroupBy_aggregate_followed_another_GroupBy_aggregate( AssertSql( """ -SELECT t0."CustomerID" AS "Key", count(*)::int AS "Count" +SELECT o1."CustomerID" AS "Key", count(*)::int AS "Count" FROM ( - SELECT t."CustomerID" + SELECT o0."CustomerID" FROM ( SELECT o."CustomerID", date_part('year', o."OrderDate")::int AS "Year" FROM "Orders" AS o - ) AS t - GROUP BY t."CustomerID", t."Year" -) AS t0 -GROUP BY t0."CustomerID" + ) AS o0 + GROUP BY o0."CustomerID", o0."Year" +) AS o1 +GROUP BY o1."CustomerID" """); } @@ -3072,9 +3075,9 @@ public override async Task GroupBy_aggregate_without_selectMany_selecting_first( SELECT min(o."OrderID") AS c FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t +) AS o1 CROSS JOIN "Orders" AS o0 -WHERE o0."OrderID" = t.c +WHERE o0."OrderID" = o1.c """); } @@ -3101,14 +3104,14 @@ public override async Task GroupBy_selecting_grouping_key_list(bool async) AssertSql( """ -SELECT t."CustomerID", o0."CustomerID", o0."OrderID" +SELECT o1."CustomerID", o0."CustomerID", o0."OrderID" FROM ( SELECT o."CustomerID" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t -LEFT JOIN "Orders" AS o0 ON t."CustomerID" = o0."CustomerID" -ORDER BY t."CustomerID" NULLS FIRST +) AS o1 +LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" +ORDER BY o1."CustomerID" NULLS FIRST """); } @@ -3118,12 +3121,12 @@ public override async Task GroupBy_with_grouping_key_using_Like(bool async) AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT o0."Key", count(*)::int AS "Count" FROM ( SELECT o."CustomerID" LIKE 'A%' AND o."CustomerID" IS NOT NULL AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -3133,12 +3136,12 @@ public override async Task GroupBy_with_grouping_key_DateTime_Day(bool async) AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT o0."Key", count(*)::int AS "Count" FROM ( SELECT date_part('day', o."OrderDate")::int AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -3160,14 +3163,14 @@ public override async Task Complex_query_with_groupBy_in_subquery1(bool async) AssertSql( """ -SELECT c."CustomerID", t."Sum", t."CustomerID" +SELECT c."CustomerID", o0."Sum", o0."CustomerID" FROM "Customers" AS c LEFT JOIN LATERAL ( SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", o."CustomerID" FROM "Orders" AS o WHERE c."CustomerID" = o."CustomerID" GROUP BY o."CustomerID" -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY c."CustomerID" NULLS FIRST """); } @@ -3178,14 +3181,14 @@ public override async Task Complex_query_with_groupBy_in_subquery2(bool async) AssertSql( """ -SELECT c."CustomerID", t."Max", t."Sum", t."CustomerID" +SELECT c."CustomerID", o0."Max", o0."Sum", o0."CustomerID" FROM "Customers" AS c LEFT JOIN LATERAL ( SELECT max(length(o."CustomerID")::int) AS "Max", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", o."CustomerID" FROM "Orders" AS o WHERE c."CustomerID" = o."CustomerID" GROUP BY o."CustomerID" -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY c."CustomerID" NULLS FIRST """); } @@ -3196,13 +3199,13 @@ public override async Task Complex_query_with_groupBy_in_subquery3(bool async) AssertSql( """ -SELECT c."CustomerID", t."Max", t."Sum", t."CustomerID" +SELECT c."CustomerID", o0."Max", o0."Sum", o0."CustomerID" FROM "Customers" AS c LEFT JOIN LATERAL ( SELECT max(length(o."CustomerID")::int) AS "Max", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", o."CustomerID" FROM "Orders" AS o GROUP BY o."CustomerID" -) AS t ON TRUE +) AS o0 ON TRUE ORDER BY c."CustomerID" NULLS FIRST """); } @@ -3282,7 +3285,7 @@ public override async Task GroupBy_scalar_subquery(bool async) AssertSql( """ -SELECT t."Key", count(*)::int AS "Count" +SELECT o0."Key", count(*)::int AS "Count" FROM ( SELECT ( SELECT c."ContactName" @@ -3290,8 +3293,8 @@ SELECT c."ContactName" WHERE c."CustomerID" = o."CustomerID" LIMIT 1) AS "Key" FROM "Orders" AS o -) AS t -GROUP BY t."Key" +) AS o0 +GROUP BY o0."Key" """); } @@ -3301,28 +3304,28 @@ public override async Task AsEnumerable_in_subquery_for_GroupBy(bool async) AssertSql( """ -SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", t2."OrderID", t2."CustomerID", t2."EmployeeID", t2."OrderDate", t2."CustomerID0" +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", s."OrderID", s."CustomerID", s."EmployeeID", s."OrderDate", s."CustomerID0" FROM "Customers" AS c LEFT JOIN LATERAL ( - SELECT t0."OrderID", t0."CustomerID", t0."EmployeeID", t0."OrderDate", t."CustomerID" AS "CustomerID0" + SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate", o1."CustomerID" AS "CustomerID0" FROM ( SELECT o."CustomerID" FROM "Orders" AS o WHERE o."CustomerID" = c."CustomerID" GROUP BY o."CustomerID" - ) AS t + ) AS o1 LEFT JOIN ( - SELECT t1."OrderID", t1."CustomerID", t1."EmployeeID", t1."OrderDate" + SELECT o2."OrderID", o2."CustomerID", o2."EmployeeID", o2."OrderDate" FROM ( SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderDate" DESC NULLS LAST) AS row FROM "Orders" AS o0 WHERE o0."CustomerID" = c."CustomerID" - ) AS t1 - WHERE t1.row <= 1 - ) AS t0 ON t."CustomerID" = t0."CustomerID" -) AS t2 ON TRUE + ) AS o2 + WHERE o2.row <= 1 + ) AS o3 ON o1."CustomerID" = o3."CustomerID" +) AS s ON TRUE WHERE c."CustomerID" LIKE 'F%' -ORDER BY c."CustomerID" NULLS FIRST, t2."CustomerID0" NULLS FIRST +ORDER BY c."CustomerID" NULLS FIRST, s."CustomerID0" NULLS FIRST """); } @@ -3414,24 +3417,24 @@ public override async Task Select_uncorrelated_collection_with_groupby_when_oute AssertSql( """ -SELECT t."City", t0."ProductID", t1.c, t1."ProductID" +SELECT s."City", p1."ProductID", p2.c, p2."ProductID" FROM ( SELECT DISTINCT c."City" FROM "Orders" AS o LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" WHERE o."CustomerID" LIKE 'A%' -) AS t +) AS s LEFT JOIN LATERAL ( SELECT p."ProductID" FROM "Products" AS p GROUP BY p."ProductID" -) AS t0 ON TRUE +) AS p1 ON TRUE LEFT JOIN LATERAL ( SELECT count(*)::int AS c, p0."ProductID" FROM "Products" AS p0 GROUP BY p0."ProductID" -) AS t1 ON TRUE -ORDER BY t."City" NULLS FIRST, t0."ProductID" NULLS FIRST +) AS p2 ON TRUE +ORDER BY s."City" NULLS FIRST, p1."ProductID" NULLS FIRST """); } @@ -3441,15 +3444,15 @@ public override async Task Select_correlated_collection_after_GroupBy_aggregate_ AssertSql( """ -SELECT t."CustomerID", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +SELECT c0."CustomerID", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM ( SELECT c."CustomerID" FROM "Customers" AS c GROUP BY c."CustomerID" HAVING c."CustomerID" LIKE 'F%' -) AS t -LEFT JOIN "Orders" AS o ON t."CustomerID" = o."CustomerID" -ORDER BY t."CustomerID" NULLS FIRST +) AS c0 +LEFT JOIN "Orders" AS o ON c0."CustomerID" = o."CustomerID" +ORDER BY c0."CustomerID" NULLS FIRST """); } @@ -3459,15 +3462,15 @@ public override async Task Select_correlated_collection_after_GroupBy_aggregate_ AssertSql( """ -SELECT t."CustomerID", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +SELECT o1."CustomerID", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" FROM ( SELECT o."CustomerID" FROM "Orders" AS o GROUP BY o."CustomerID" HAVING o."CustomerID" LIKE 'F%' -) AS t -LEFT JOIN "Orders" AS o0 ON t."CustomerID" = o0."CustomerID" -ORDER BY t."CustomerID" NULLS FIRST +) AS o1 +LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" +ORDER BY o1."CustomerID" NULLS FIRST """); } @@ -3481,7 +3484,7 @@ public override async Task Complex_query_with_group_by_in_subquery5(bool async) AssertSql( """ -SELECT t.c, t."ProductID", t0."CustomerID", t0."City" +SELECT s.c, s."ProductID", c1."CustomerID", c1."City" FROM ( SELECT COALESCE(sum(o."ProductID" + o."OrderID" * 1000), 0)::int AS c, o."ProductID", min(o."OrderID" / 100) AS c0 FROM "Order Details" AS o @@ -3489,13 +3492,13 @@ SELECT COALESCE(sum(o."ProductID" + o."OrderID" * 1000), 0)::int AS c, o."Produc LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" WHERE c."CustomerID" = 'ALFKI' GROUP BY o."ProductID" -) AS t +) AS s LEFT JOIN LATERAL ( SELECT c0."CustomerID", c0."City" FROM "Customers" AS c0 - WHERE length(c0."CustomerID")::int < t.c0 -) AS t0 ON TRUE -ORDER BY t."ProductID" NULLS FIRST, t0."CustomerID" NULLS FIRST + WHERE length(c0."CustomerID")::int < s.c0 +) AS c1 ON TRUE +ORDER BY s."ProductID" NULLS FIRST, c1."CustomerID" NULLS FIRST """); } @@ -3505,27 +3508,27 @@ public override async Task Complex_query_with_groupBy_in_subquery4(bool async) AssertSql( """ -SELECT c."CustomerID", t1."Sum", t1."Count", t1."Key" +SELECT c."CustomerID", s1."Sum", s1."Count", s1."Key" FROM "Customers" AS c LEFT JOIN LATERAL ( - SELECT COALESCE(sum(t."OrderID"), 0)::int AS "Sum", ( + SELECT COALESCE(sum(s."OrderID"), 0)::int AS "Sum", ( SELECT count(*)::int FROM ( - SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", c1."CustomerID" AS "CustomerID0", c1."Address", c1."City", c1."CompanyName", c1."ContactName", c1."ContactTitle", c1."Country", c1."Fax", c1."Phone", c1."PostalCode", c1."Region", COALESCE(c1."City", '') || COALESCE(o0."CustomerID", '') AS "Key" + SELECT o0."CustomerID", COALESCE(c1."City", '') || COALESCE(o0."CustomerID", '') AS "Key" FROM "Orders" AS o0 LEFT JOIN "Customers" AS c1 ON o0."CustomerID" = c1."CustomerID" WHERE c."CustomerID" = o0."CustomerID" - ) AS t0 - LEFT JOIN "Customers" AS c0 ON t0."CustomerID" = c0."CustomerID" - WHERE (t."Key" = t0."Key" OR (t."Key" IS NULL AND t0."Key" IS NULL)) AND COALESCE(c0."City", '') || COALESCE(t0."CustomerID", '') LIKE 'Lon%') AS "Count", t."Key" + ) AS s0 + LEFT JOIN "Customers" AS c2 ON s0."CustomerID" = c2."CustomerID" + WHERE (s."Key" = s0."Key" OR (s."Key" IS NULL AND s0."Key" IS NULL)) AND COALESCE(c2."City", '') || COALESCE(s0."CustomerID", '') LIKE 'Lon%') AS "Count", s."Key" FROM ( - SELECT o."OrderID", COALESCE(c2."City", '') || COALESCE(o."CustomerID", '') AS "Key" + SELECT o."OrderID", COALESCE(c0."City", '') || COALESCE(o."CustomerID", '') AS "Key" FROM "Orders" AS o - LEFT JOIN "Customers" AS c2 ON o."CustomerID" = c2."CustomerID" + LEFT JOIN "Customers" AS c0 ON o."CustomerID" = c0."CustomerID" WHERE c."CustomerID" = o."CustomerID" - ) AS t - GROUP BY t."Key" -) AS t1 ON TRUE + ) AS s + GROUP BY s."Key" +) AS s1 ON TRUE ORDER BY c."CustomerID" NULLS FIRST """); } @@ -3606,12 +3609,12 @@ public override async Task Final_GroupBy_complex_key_entity(bool async) AssertSql( """ -SELECT t."City", t."Region", t."Constant", t."CustomerID", t."Address", t."CompanyName", t."ContactName", t."ContactTitle", t."Country", t."Fax", t."Phone", t."PostalCode" +SELECT c0."City", c0."Region", c0."Constant", c0."CustomerID", c0."Address", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode" FROM ( SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", 1 AS "Constant" FROM "Customers" AS c -) AS t -ORDER BY t."City" NULLS FIRST, t."Region" NULLS FIRST, t."Constant" NULLS FIRST +) AS c0 +ORDER BY c0."City" NULLS FIRST, c0."Region" NULLS FIRST, c0."Constant" NULLS FIRST """); } @@ -3621,12 +3624,12 @@ public override async Task Final_GroupBy_nominal_type_entity(bool async) AssertSql( """ -SELECT t."City", t."Constant", t."CustomerID", t."Address", t."CompanyName", t."ContactName", t."ContactTitle", t."Country", t."Fax", t."Phone", t."PostalCode", t."Region" +SELECT c0."City", c0."Constant", c0."CustomerID", c0."Address", c0."CompanyName", c0."ContactName", c0."ContactTitle", c0."Country", c0."Fax", c0."Phone", c0."PostalCode", c0."Region" FROM ( SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", 1 AS "Constant" FROM "Customers" AS c -) AS t -ORDER BY t."City" NULLS FIRST, t."Constant" NULLS FIRST +) AS c0 +ORDER BY c0."City" NULLS FIRST, c0."Constant" NULLS FIRST """); } @@ -3676,13 +3679,13 @@ public override async Task Final_GroupBy_property_entity_projecting_collection_c AssertSql( """ -SELECT c."City", c."CustomerID", t."OrderID", t."CustomerID", t."EmployeeID", t."OrderDate" +SELECT c."City", c."CustomerID", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" FROM "Customers" AS c LEFT JOIN ( SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o WHERE o."OrderID" < 11000 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o0 ON c."CustomerID" = o0."CustomerID" WHERE c."Country" = 'USA' ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST """); @@ -3694,21 +3697,21 @@ public override async Task Final_GroupBy_property_entity_projecting_collection_a AssertSql( """ -SELECT c."City", c."CustomerID", t."OrderID", t."CustomerID", t."EmployeeID", t."OrderDate", t0."OrderID", t0."CustomerID", t0."EmployeeID", t0."OrderDate" +SELECT c."City", c."CustomerID", o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate", o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" FROM "Customers" AS c LEFT JOIN ( SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o WHERE o."OrderID" < 11000 -) AS t ON c."CustomerID" = t."CustomerID" +) AS o1 ON c."CustomerID" = o1."CustomerID" LEFT JOIN ( - SELECT t1."OrderID", t1."CustomerID", t1."EmployeeID", t1."OrderDate" + SELECT o2."OrderID", o2."CustomerID", o2."EmployeeID", o2."OrderDate" FROM ( SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderDate" DESC NULLS LAST) AS row FROM "Orders" AS o0 - ) AS t1 - WHERE t1.row <= 1 -) AS t0 ON c."CustomerID" = t0."CustomerID" + ) AS o2 + WHERE o2.row <= 1 +) AS o3 ON c."CustomerID" = o3."CustomerID" WHERE c."Country" = 'USA' ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST """); @@ -3769,29 +3772,29 @@ public override async Task GroupBy_complex_key_without_aggregate(bool async) AssertSql( """ -SELECT t0."Key", t1."OrderID", t1."CustomerID", t1."EmployeeID", t1."OrderDate", t1."CustomerID0" +SELECT s1."Key", s3."OrderID", s3."CustomerID", s3."EmployeeID", s3."OrderDate", s3."CustomerID0" FROM ( - SELECT t."Key" + SELECT s."Key" FROM ( SELECT substring(c."CustomerID", 1, 1) AS "Key" FROM "Orders" AS o LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" - ) AS t - GROUP BY t."Key" -) AS t0 + ) AS s + GROUP BY s."Key" +) AS s1 LEFT JOIN ( - SELECT t2."OrderID", t2."CustomerID", t2."EmployeeID", t2."OrderDate", t2."CustomerID0", t2."Key" + SELECT s2."OrderID", s2."CustomerID", s2."EmployeeID", s2."OrderDate", s2."CustomerID0", s2."Key" FROM ( - SELECT t3."OrderID", t3."CustomerID", t3."EmployeeID", t3."OrderDate", t3."CustomerID0", t3."Key", ROW_NUMBER() OVER(PARTITION BY t3."Key" ORDER BY t3."OrderID" NULLS FIRST, t3."CustomerID0" NULLS FIRST) AS row + SELECT s0."OrderID", s0."CustomerID", s0."EmployeeID", s0."OrderDate", s0."CustomerID0", s0."Key", ROW_NUMBER() OVER(PARTITION BY s0."Key" ORDER BY s0."OrderID" NULLS FIRST, s0."CustomerID0" NULLS FIRST) AS row FROM ( SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", c0."CustomerID" AS "CustomerID0", substring(c0."CustomerID", 1, 1) AS "Key" FROM "Orders" AS o0 LEFT JOIN "Customers" AS c0 ON o0."CustomerID" = c0."CustomerID" - ) AS t3 - ) AS t2 - WHERE 1 < t2.row AND t2.row <= 3 -) AS t1 ON t0."Key" = t1."Key" -ORDER BY t0."Key" NULLS FIRST, t1."OrderID" NULLS FIRST + ) AS s0 + ) AS s2 + WHERE 1 < s2.row AND s2.row <= 3 +) AS s3 ON s1."Key" = s3."Key" +ORDER BY s1."Key" NULLS FIRST, s3."OrderID" NULLS FIRST """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs index 6fec39611..521f2ff6c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs @@ -24,7 +24,7 @@ public override async Task Query_expression_with_to_string_and_contains(bool asy """ SELECT o."CustomerID" FROM "Orders" AS o -WHERE o."OrderDate" IS NOT NULL AND o."EmployeeID"::text LIKE '%7%' +WHERE o."OrderDate" IS NOT NULL AND COALESCE(o."EmployeeID"::text, '') LIKE '%7%' """); } @@ -188,14 +188,26 @@ await Assert.ThrowsAsync( AssertSql( """ -SELECT t."CustomerID", o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +SELECT o0."CustomerID", o1."OrderID", o1."CustomerID", o1."EmployeeID", o1."OrderDate" FROM ( SELECT DISTINCT o."CustomerID" FROM "Orders" AS o WHERE o."OrderID" < 10300 -) AS t -LEFT JOIN "Orders" AS o0 ON t."CustomerID" = o0."CustomerID" -ORDER BY t."CustomerID" NULLS FIRST +) AS o0 +LEFT JOIN "Orders" AS o1 ON o0."CustomerID" = o1."CustomerID" +ORDER BY o0."CustomerID" NULLS FIRST +"""); + } + + public override async Task Where_bitwise_binary_xor(bool async) + { + await base.Where_bitwise_binary_xor(async); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +WHERE (o."OrderID" # 1) = 10249 """); } @@ -286,9 +298,8 @@ public async Task Array_Any_Like(bool async) var result = async ? await query.ToListAsync() : query.ToList(); Assert.Equal( - new[] - { - "ANATR", + [ + "ANATR", "BERGS", "BOLID", "CACTU", @@ -310,15 +321,15 @@ public async Task Array_Any_Like(bool async) "TORTU", "TRADH", "WANDK" - }, result.Select(e => e.CustomerID)); + ], result.Select(e => e.CustomerID)); AssertSql( """ -@__collection_0={ 'A%', 'B%', 'C%' } (DbType = Object) +@__collection_1={ 'A%', 'B%', 'C%' } (DbType = Object) SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c -WHERE c."Address" LIKE ANY (@__collection_0) +WHERE c."Address" LIKE ANY (@__collection_1) """); } @@ -336,11 +347,33 @@ public async Task Array_All_Like(bool async) AssertSql( """ -@__collection_0={ 'A%', 'B%', 'C%' } (DbType = Object) +@__collection_1={ 'A%', 'B%', 'C%' } (DbType = Object) + +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."Address" LIKE ALL (@__collection_1) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_All_Like_negated(bool async) + { + await using var context = CreateContext(); + + var collection = new[] { "A%", "B%", "C%" }; + var query = context.Set().Where(c => !collection.All(y => EF.Functions.Like(c.Address, y))); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.NotEmpty(result); + + AssertSql( + """ +@__collection_1={ 'A%', 'B%', 'C%' } (DbType = Object) SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c -WHERE c."Address" LIKE ALL (@__collection_0) +WHERE NOT (c."Address" LIKE ALL (@__collection_1)) """); } @@ -355,9 +388,8 @@ public async Task Array_Any_ILike(bool async) var result = async ? await query.ToListAsync() : query.ToList(); Assert.Equal( - new[] - { - "ANATR", + [ + "ANATR", "BERGS", "BOLID", "CACTU", @@ -379,15 +411,44 @@ public async Task Array_Any_ILike(bool async) "TORTU", "TRADH", "WANDK" - }, result.Select(e => e.CustomerID)); + ], result.Select(e => e.CustomerID)); + + AssertSql( + """ +@__collection_1={ 'a%', 'b%', 'c%' } (DbType = Object) + +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE c."Address" ILIKE ANY (@__collection_1) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Any_ILike_negated(bool async) + { + await using var context = CreateContext(); + + var collection = new[] { "a%", "b%", "c%" }; + var query = context.Set().Where(c => !collection.Any(y => EF.Functions.ILike(c.Address, y))); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Equal( + [ + "ALFKI", + "ANTON", + "AROUT", + "BLAUS", + "BLONP" + ], result.Select(e => e.CustomerID).Order().Take(5)); AssertSql( """ -@__collection_0={ 'a%', 'b%', 'c%' } (DbType = Object) +@__collection_1={ 'a%', 'b%', 'c%' } (DbType = Object) SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c -WHERE c."Address" ILIKE ANY (@__collection_0) +WHERE NOT (c."Address" ILIKE ANY (@__collection_1)) """); } @@ -405,11 +466,11 @@ public async Task Array_All_ILike(bool async) AssertSql( """ -@__collection_0={ 'a%', 'b%', 'c%' } (DbType = Object) +@__collection_1={ 'a%', 'b%', 'c%' } (DbType = Object) SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" FROM "Customers" AS c -WHERE c."Address" ILIKE ALL (@__collection_0) +WHERE c."Address" ILIKE ALL (@__collection_1) """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs index 0c2142a98..e8d412e21 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; public class NorthwindQueryNpgsqlFixture : NorthwindQueryRelationalFixture - where TModelCustomizer : IModelCustomizer, new() + where TModelCustomizer : ITestModelCustomizer, new() { protected override ITestStoreFactory TestStoreFactory => NpgsqlNorthwindTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs index 543082d04..316ffb4a9 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs @@ -135,7 +135,7 @@ await AssertQuery( AssertSql( """ -SELECT t0."OrderID", t0."CustomerID" +SELECT u0."OrderID", u0."CustomerID" FROM ( SELECT NULL::int AS "OrderID", o."CustomerID" FROM "Orders" AS o @@ -145,9 +145,9 @@ await AssertQuery( UNION SELECT o1."OrderID", o1."CustomerID" FROM "Orders" AS o1 -) AS t0 -WHERE t0."CustomerID" = ( - SELECT t1."CustomerID" +) AS u0 +WHERE u0."CustomerID" = ( + SELECT u2."CustomerID" FROM ( SELECT NULL::int AS "OrderID", o2."CustomerID" FROM "Orders" AS o2 @@ -157,10 +157,10 @@ SELECT t1."CustomerID" UNION SELECT o4."OrderID", o4."CustomerID" FROM "Orders" AS o4 - ) AS t1 - ORDER BY t1."CustomerID" NULLS FIRST - LIMIT 1) OR (t0."CustomerID" IS NULL AND ( - SELECT t1."CustomerID" + ) AS u2 + ORDER BY u2."CustomerID" NULLS FIRST + LIMIT 1) OR (u0."CustomerID" IS NULL AND ( + SELECT u2."CustomerID" FROM ( SELECT NULL::int AS "OrderID", o2."CustomerID" FROM "Orders" AS o2 @@ -170,8 +170,8 @@ SELECT t1."CustomerID" UNION SELECT o4."OrderID", o4."CustomerID" FROM "Orders" AS o4 - ) AS t1 - ORDER BY t1."CustomerID" NULLS FIRST + ) AS u2 + ORDER BY u2."CustomerID" NULLS FIRST LIMIT 1) IS NULL) """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs index 122773d93..d9f1bdd92 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs @@ -29,10 +29,10 @@ public override async Task SqlQuery_composed_Contains(bool async) SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o WHERE o."OrderID" IN ( - SELECT t."Value" + SELECT s."Value" FROM ( SELECT "ProductID" AS "Value" FROM "Products" - ) AS t + ) AS s ) """); } @@ -43,11 +43,11 @@ public override async Task SqlQuery_composed_Join(bool async) AssertSql( """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate", t."Value"::int AS p +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate", s."Value"::int AS p FROM "Orders" AS o INNER JOIN ( SELECT "ProductID" AS "Value" FROM "Products" -) AS t ON o."OrderID" = t."Value"::int +) AS s ON o."OrderID" = s."Value"::int """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs index cc6da3a3b..552db3a89 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs @@ -29,7 +29,6 @@ public override async Task Where_datetime_today(bool async) """ SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" FROM "Employees" AS e -WHERE date_trunc('day', now()::timestamp) = date_trunc('day', now()::timestamp) """); } @@ -172,15 +171,19 @@ public override Task Where_datetimeoffset_utcnow(bool async) public override async Task Where_bitwise_xor(bool async) { - // Cannot eval 'where (([c].CustomerID == \"ALFKI\") ^ True)'. Issue #16645. - await AssertTranslationFailed(() => base.Where_bitwise_xor(async)); + await base.Where_bitwise_xor(async); - AssertSql(); + AssertSql( + """ +SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +WHERE (c."CustomerID" = 'ALFKI') <> TRUE +"""); } public override async Task Where_compare_constructed_equal(bool async) { - // Anonymous type to constant comparison. Issue #14672. + // Anonymous type to constant comparison. Issue #14672. await AssertTranslationFailed(() => base.Where_compare_constructed_equal(async)); AssertSql(); @@ -328,11 +331,11 @@ public async Task Row_value_GreaterThan_with_parameter() AssertSql( """ -@__city1_1='Buenos Aires' +@__city1_0='Buenos Aires' SELECT count(*)::int FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > (@__city1_1, 'OCEAN') +WHERE (c."City", c."CustomerID") > (@__city1_0, 'OCEAN') """); } @@ -352,11 +355,11 @@ public async Task Row_value_GreaterThan_with_parameter_with_ValueTuple_construct AssertSql( """ -@__city1_1='Buenos Aires' +@__city1_0='Buenos Aires' SELECT count(*)::int FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > (@__city1_1, 'OCEAN') +WHERE (c."City", c."CustomerID") > (@__city1_0, 'OCEAN') """); } @@ -481,10 +484,29 @@ public async Task Row_value_not_equals() { await using var ctx = CreateContext(); + // Everything is non-nullable, so we use the nicer row value comparison syntax + _ = await ctx.Customers + .Where(c => !ValueTuple.Create(c.CustomerID, c.CustomerID).Equals(ValueTuple.Create("OCEAN", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."CustomerID", c."CustomerID") <> ('OCEAN', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_not_equals_with_nullable() + { + await using var ctx = CreateContext(); + _ = await ctx.Customers .Where(c => !ValueTuple.Create(c.City, c.CustomerID).Equals(ValueTuple.Create("Buenos Aires", "OCEAN"))) .CountAsync(); + // City is nullable, so we must extract that comparison out of the row value AssertSql( """ SELECT count(*)::int diff --git a/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs index 8c465a89b..5a7e3dadf 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class NullKeysNpgsqlTest : NullKeysTestBase +public class NullKeysNpgsqlTest(NullKeysNpgsqlTest.NullKeysNpgsqlFixture fixture) + : NullKeysTestBase(fixture) { - public NullKeysNpgsqlTest(NullKeysNpgsqlFixture fixture) - : base(fixture) - { - } - public class NullKeysNpgsqlFixture : NullKeysFixtureBase { protected override string StoreName { get; } = "StringsContext"; diff --git a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs index a5bb64c03..eb0ba354a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs @@ -1,10 +1,12 @@ using Microsoft.EntityFrameworkCore.TestModels.NullSemanticsModel; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; // ReSharper disable once UnusedMember.Global -public class NullSemanticsQueryNpgsqlTest : NullSemanticsQueryTestBase +public class NullSemanticsQueryNpgsqlTest : NullSemanticsQueryTestBase { public NullSemanticsQueryNpgsqlTest(NullSemanticsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) @@ -180,4 +182,26 @@ protected override NullSemanticsContext CreateContext(bool useRelationalNulls = return context; } + + public class NullSemanticsQueryNpgsqlFixture : NullSemanticsQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // The base implementations maps this function to bool constants with BoolTypeMapping.Default which doesn't work with PG; + // override to use NpgsqlBoolTypeMapping instead. + modelBuilder.HasDbFunction( + typeof(NullSemanticsQueryFixtureBase).GetMethod(nameof(BoolSwitch))!, + b => b.HasTranslation(args => new CaseExpression( + operand: args[0], + [ + new CaseWhenClause(new SqlConstantExpression(true, typeMapping: NpgsqlBoolTypeMapping.Default), args[1]), + new CaseWhenClause(new SqlConstantExpression(false, typeMapping: NpgsqlBoolTypeMapping.Default), args[2]) + ]))); + } + } } diff --git a/test/EFCore.PG.FunctionalTests/Query/OperatorsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OperatorsQueryNpgsqlTest.cs index ab04dc9df..97bdda412 100644 --- a/test/EFCore.PG.FunctionalTests/Query/OperatorsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/OperatorsQueryNpgsqlTest.cs @@ -15,21 +15,46 @@ public override async Task Bitwise_and_on_expression_with_like_and_null_check_be { await base.Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false(); - AssertSql(""); + AssertSql( + """ +SELECT o."Value" AS "Value1", o0."Value" AS "Value2", o1."Value" AS "Value3" +FROM "OperatorEntityString" AS o +CROSS JOIN "OperatorEntityString" AS o0 +CROSS JOIN "OperatorEntityBool" AS o1 +WHERE ((o0."Value" LIKE 'B' AND o0."Value" IS NOT NULL) OR o1."Value") AND o."Value" IS NOT NULL +ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST +"""); } public override async Task Complex_predicate_with_bitwise_and_modulo_and_negation() { await base.Complex_predicate_with_bitwise_and_modulo_and_negation(); - AssertSql(""); + AssertSql( + """ +SELECT o."Value" AS "Value0", o0."Value" AS "Value1", o1."Value" AS "Value2", o2."Value" AS "Value3" +FROM "OperatorEntityLong" AS o +CROSS JOIN "OperatorEntityLong" AS o0 +CROSS JOIN "OperatorEntityLong" AS o1 +CROSS JOIN "OperatorEntityLong" AS o2 +WHERE (o0."Value" % 2) / o."Value" & ((o2."Value" | o1."Value") - o."Value") - o1."Value" * o1."Value" >= ((o0."Value" / (~o2."Value")) % 2) % ((~o."Value") + 1) +ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST, o2."Id" NULLS FIRST +"""); } public override async Task Complex_predicate_with_bitwise_and_arithmetic_operations() { await base.Complex_predicate_with_bitwise_and_arithmetic_operations(); - AssertSql(""); + AssertSql( + """ +SELECT o."Value" AS "Value0", o0."Value" AS "Value1", o1."Value" AS "Value2" +FROM "OperatorEntityInt" AS o +CROSS JOIN "OperatorEntityInt" AS o0 +CROSS JOIN "OperatorEntityBool" AS o1 +WHERE (o0."Value" & o."Value" + o."Value" & o."Value") / 1 > o0."Value" & 10 AND o1."Value" +ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST +"""); } public override async Task Or_on_two_nested_binaries_and_another_simple_comparison() @@ -44,7 +69,7 @@ CROSS JOIN "OperatorEntityString" AS o0 CROSS JOIN "OperatorEntityString" AS o1 CROSS JOIN "OperatorEntityString" AS o2 CROSS JOIN "OperatorEntityInt" AS o3 -WHERE ((o."Value" = 'A' AND o."Value" IS NOT NULL AND o0."Value" = 'A' AND o0."Value" IS NOT NULL) OR (o1."Value" = 'B' AND o1."Value" IS NOT NULL AND o2."Value" = 'B' AND o2."Value" IS NOT NULL)) AND o3."Value" = 2 +WHERE ((o."Value" = 'A' AND o0."Value" = 'A') OR (o1."Value" = 'B' AND o2."Value" = 'B')) AND o3."Value" = 2 ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST, o2."Id" NULLS FIRST, o3."Id" NULLS FIRST """); } @@ -125,7 +150,42 @@ LIMIT 2 """); } - protected override void Seed(OperatorsContext ctx) + [ConditionalFact] + public virtual async Task AtTimeZone_and_addition() + { + var contextFactory = await InitializeAsync( + seed: async context => + { + context.Set().AddRange( + new OperatorEntityDateTime { Id = 1, Value = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, + new OperatorEntityDateTime { Id = 2, Value = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) }); + await context.SaveChangesAsync(); + }, + onModelCreating: modelBuilder => modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever()); + + await using var context = contextFactory.CreateContext(); + + var result = await context.Set() + .Where(b => new DateOnly(2020, 1, 15) > DateOnly.FromDateTime(b.Value.AddDays(1))) + .SingleAsync(); + + Assert.Equal(1, result.Id); + + AssertSql( + """ +SELECT o."Id", o."Value" +FROM "OperatorEntityDateTime" AS o +WHERE DATE '2020-01-15' > CAST((o."Value" + INTERVAL '1 days') AT TIME ZONE 'UTC' AS date) +LIMIT 2 +"""); + } + + public class OperatorEntityDateTime : OperatorEntityBase + { + public DateTime Value { get; set; } + } + + protected override async Task Seed(OperatorsContext ctx) { ctx.Set().AddRange(ExpectedData.OperatorEntitiesString); ctx.Set().AddRange(ExpectedData.OperatorEntitiesInt); @@ -135,6 +195,6 @@ protected override void Seed(OperatorsContext ctx) ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableBool); // ctx.Set().AddRange(ExpectedData.OperatorEntitiesDateTimeOffset); - ctx.SaveChanges(); + await ctx.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs index e5a0429a7..fd15a3621 100644 --- a/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class OwnedQueryNpgsqlTest : OwnedQueryRelationalTestBase +public class OwnedQueryNpgsqlTest(OwnedQueryNpgsqlTest.OwnedQueryNpgsqlFixture fixture) + : OwnedQueryRelationalTestBase(fixture) { - public OwnedQueryNpgsqlTest(OwnedQueryNpgsqlFixture fixture) - : base(fixture) - { - } - public class OwnedQueryNpgsqlFixture : RelationalOwnedQueryFixture { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs new file mode 100644 index 000000000..c64f357b1 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs @@ -0,0 +1,2013 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class PrecompiledQueryNpgsqlTest( + PrecompiledQueryNpgsqlTest.PrecompiledQueryNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => true; + + #region Expression types + + public override async Task BinaryExpression() + { + await base.BinaryExpression(); + + AssertSql( + """ +@__id_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > @__id_0 +"""); + } + + public override async Task Conditional_no_evaluatable() + { + await base.Conditional_no_evaluatable(); + + AssertSql( + """ +SELECT CASE + WHEN b."Id" = 2 THEN 'yes' + ELSE 'no' +END +FROM "Blogs" AS b +"""); + } + + public override async Task Conditional_contains_captured_variable() + { + await base.Conditional_contains_captured_variable(); + + AssertSql( + """ +@__yes_0='yes' + +SELECT CASE + WHEN b."Id" = 2 THEN @__yes_0 + ELSE 'no' +END +FROM "Blogs" AS b +"""); + } + + public override async Task Invoke_no_evaluatability_is_not_supported() + { + await base.Invoke_no_evaluatability_is_not_supported(); + + AssertSql(); + } + + public override async Task ListInit_no_evaluatability() + { + await base.ListInit_no_evaluatability(); + + AssertSql( + """ +SELECT b."Id", b."Id" + 1 +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_with_evaluatable_with_captured_variable() + { + await base.ListInit_with_evaluatable_with_captured_variable(); + + AssertSql( + """ +SELECT b."Id" +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_with_evaluatable_without_captured_variable() + { + await base.ListInit_with_evaluatable_without_captured_variable(); + + AssertSql( + """ +SELECT b."Id" +FROM "Blogs" AS b +"""); + } + + public override async Task ListInit_fully_evaluatable() + { + await base.ListInit_fully_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" IN (7, 8) +LIMIT 2 +"""); + } + + public override async Task MethodCallExpression_no_evaluatability() + { + await base.MethodCallExpression_no_evaluatability(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" IS NOT NULL AND left(b."Name", length(b."Name")) = b."Name" +"""); + } + + public override async Task MethodCallExpression_with_evaluatable_with_captured_variable() + { + await base.MethodCallExpression_with_evaluatable_with_captured_variable(); + + AssertSql( + """ +@__pattern_0_startswith='foo%' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__pattern_0_startswith +"""); + } + + public override async Task MethodCallExpression_with_evaluatable_without_captured_variable() + { + await base.MethodCallExpression_with_evaluatable_without_captured_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE 'foo%' +"""); + } + + public override async Task MethodCallExpression_fully_evaluatable() + { + await base.MethodCallExpression_fully_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task New_with_no_arguments() + { + await base.New_with_no_arguments(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 0 +"""); + } + + public override async Task Where_New_with_captured_variable() + { + await base.Where_New_with_captured_variable(); + + AssertSql(); + } + + public override async Task Select_New_with_captured_variable() + { + await base.Select_New_with_captured_variable(); + + AssertSql( + """ +SELECT b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_no_evaluatable() + { + await base.MemberInit_no_evaluatable(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_contains_captured_variable() + { + await base.MemberInit_contains_captured_variable(); + + AssertSql( + """ +@__id_0='8' + +SELECT @__id_0 AS "Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_evaluatable_as_constant() + { + await base.MemberInit_evaluatable_as_constant(); + + AssertSql( + """ +SELECT 1 AS "Id", 'foo' AS "Name" +FROM "Blogs" AS b +"""); + } + + public override async Task MemberInit_evaluatable_as_parameter() + { + await base.MemberInit_evaluatable_as_parameter(); + + AssertSql( + """ +SELECT 1 +FROM "Blogs" AS b +"""); + } + + public override async Task NewArray() + { + await base.NewArray(); + + AssertSql( + """ +@__i_0='8' + +SELECT ARRAY[b."Id",b."Id" + @__i_0]::integer[] +FROM "Blogs" AS b +"""); + } + + public override async Task Unary() + { + await base.Unary(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id"::smallint = 8 +"""); + } + + public virtual async Task Collate() + { + await Test("""_ = context.Blogs.Where(b => EF.Functions.Collate(b.Name, "German_PhoneBook_CI_AS") == "foo").ToList();"""); + + AssertSql(); + } + + #endregion Expression types + + #region Terminating operators + + public override async Task Terminating_AsEnumerable() + { + await base.Terminating_AsEnumerable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AsAsyncEnumerable_on_DbSet() + { + await base.Terminating_AsAsyncEnumerable_on_DbSet(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AsAsyncEnumerable_on_IQueryable() + { + await base.Terminating_AsAsyncEnumerable_on_IQueryable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Foreach_sync_over_operator() + { + await base.Foreach_sync_over_operator(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_ToArray() + { + await base.Terminating_ToArray(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToArrayAsync() + { + await base.Terminating_ToArrayAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToDictionary() + { + await base.Terminating_ToDictionary(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToDictionaryAsync() + { + await base.Terminating_ToDictionaryAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task ToDictionary_over_anonymous_type() + { + await base.ToDictionary_over_anonymous_type(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task ToDictionaryAsync_over_anonymous_type() + { + await base.ToDictionaryAsync_over_anonymous_type(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToHashSet() + { + await base.Terminating_ToHashSet(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToHashSetAsync() + { + await base.Terminating_ToHashSetAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToLookup() + { + await base.Terminating_ToLookup(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToList() + { + await base.Terminating_ToList(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ToListAsync() + { + await base.Terminating_ToListAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task Foreach_sync_over_DbSet_property_is_not_supported() + { + await base.Foreach_sync_over_DbSet_property_is_not_supported(); + + AssertSql(); + } + + public override async Task Foreach_async_is_not_supported() + { + await base.Foreach_async_is_not_supported(); + + AssertSql(); + } + + #endregion Terminating operators + + #region Reducing terminating operators + + public override async Task Terminating_All() + { + await base.Terminating_All(); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 7) +""", + // + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 8) +"""); + } + + public override async Task Terminating_AllAsync() + { + await base.Terminating_AllAsync(); +AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 7) +""", + // + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" <= 8) +"""); + } + + public override async Task Terminating_Any() + { + await base.Terminating_Any(); +AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +"""); + } + + public override async Task Terminating_AnyAsync() + { + await base.Terminating_AnyAsync(); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" > 7) +""", + // + """ +SELECT EXISTS ( + SELECT 1 + FROM "Blogs" AS b + WHERE b."Id" < 7) +"""); + } + + public override async Task Terminating_Average() + { + await base.Terminating_Average(); + + AssertSql( + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +""", + // + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_AverageAsync() + { + await base.Terminating_AverageAsync(); + + AssertSql( + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +""", + // + """ +SELECT avg(b."Id"::double precision) +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Contains() + { + await base.Terminating_Contains(); + + AssertSql( + """ +@__p_0='8' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +""", + // + """ +@__p_0='7' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +"""); + } + + public override async Task Terminating_ContainsAsync() + { + await base.Terminating_ContainsAsync(); + + AssertSql( + """ +@__p_0='8' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +""", + // + """ +@__p_0='7' + +SELECT @__p_0 IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +"""); + } + + public override async Task Terminating_Count() + { + await base.Terminating_Count(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Blogs" AS b +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_CountAsync() + { + await base.Terminating_CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Blogs" AS b +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" > 8 +"""); + } + + public override async Task Terminating_ElementAt() + { + await base.Terminating_ElementAt(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtAsync() + { + await base.Terminating_ElementAtAsync(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtOrDefault() + { + await base.Terminating_ElementAtOrDefault(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_ElementAtOrDefaultAsync() + { + await base.Terminating_ElementAtOrDefaultAsync(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +""", + // + """ +@__p_0='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @__p_0 +"""); + } + + public override async Task Terminating_First() + { + await base.Terminating_First(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstAsync() + { + await base.Terminating_FirstAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstOrDefault() + { + await base.Terminating_FirstOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_FirstOrDefaultAsync() + { + await base.Terminating_FirstOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 1 +"""); + } + + public override async Task Terminating_GetEnumerator() + { + await base.Terminating_GetEnumerator(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_Last() + { + await base.Terminating_Last(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastAsync() + { + await base.Terminating_LastAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastOrDefault() + { + await base.Terminating_LastOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LastOrDefaultAsync() + { + await base.Terminating_LastOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +ORDER BY b."Id" DESC NULLS LAST +LIMIT 1 +"""); + } + + public override async Task Terminating_LongCount() + { + await base.Terminating_LongCount(); + + AssertSql( + """ +SELECT count(*) +FROM "Blogs" AS b +""", + // + """ +SELECT count(*) +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_LongCountAsync() + { + await base.Terminating_LongCountAsync(); + + AssertSql( + """ +SELECT count(*) +FROM "Blogs" AS b +""", + // + """ +SELECT count(*) +FROM "Blogs" AS b +WHERE b."Id" = 8 +"""); + } + + public override async Task Terminating_Max() + { + await base.Terminating_Max(); +AssertSql( + """ +SELECT max(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT max(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_MaxAsync() + { + await base.Terminating_MaxAsync(); + + AssertSql( + """ +SELECT max(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT max(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Min() + { + await base.Terminating_Min(); + + AssertSql( + """ +SELECT min(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT min(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_MinAsync() + { + await base.Terminating_MinAsync(); + + AssertSql( + """ +SELECT min(b."Id") +FROM "Blogs" AS b +""", + // + """ +SELECT min(b."Id") +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_Single() + { + await base.Terminating_Single(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleAsync() + { + await base.Terminating_SingleAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleOrDefault() + { + await base.Terminating_SingleOrDefault(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_SingleOrDefaultAsync() + { + await base.Terminating_SingleOrDefaultAsync(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 8 +LIMIT 2 +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = 7 +LIMIT 2 +"""); + } + + public override async Task Terminating_Sum() + { + await base.Terminating_Sum(); + + AssertSql( + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +""", + // + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_SumAsync() + { + await base.Terminating_SumAsync(); + + AssertSql( + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +""", + // + """ +SELECT COALESCE(sum(b."Id"), 0)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteDelete() + { + await base.Terminating_ExecuteDelete(); + + AssertSql( + """ +DELETE FROM "Blogs" AS b +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteDeleteAsync() + { + await base.Terminating_ExecuteDeleteAsync(); + + AssertSql( + """ +DELETE FROM "Blogs" AS b +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +"""); + } + + public override async Task Terminating_ExecuteUpdate() + { + await base.Terminating_ExecuteUpdate(); + + AssertSql( + """ +@__suffix_0='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @__suffix_0 +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' +"""); + } + + public override async Task Terminating_ExecuteUpdateAsync() + { + await base.Terminating_ExecuteUpdateAsync(); + + AssertSql( + """ +@__suffix_0='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @__suffix_0 +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' +"""); + } + + #endregion Reducing terminating operators + + #region SQL expression quotability + + public override async Task Union() + { + await base.Union(); + + AssertSql( + """ +SELECT u."Id", u."BlogId", u."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + UNION + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST +"""); + } + + public override async Task UnionOnEntitiesWithJson() + { + await base.UnionOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [u].[Id], [u].[Name], [u].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + UNION + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] < 10 +) AS [u] +ORDER BY [u].[Id] +"""); + } + + public override async Task Concat() + { + await base.Concat(); + + AssertSql( + """ +SELECT u."Id", u."BlogId", u."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + UNION ALL + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 21 +) AS u +ORDER BY u."Id" NULLS FIRST +"""); + } + + public override async Task ConcatOnEntitiesWithJson() + { + await base.ConcatOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [u].[Id], [u].[Name], [u].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + UNION ALL + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] < 10 +) AS [u] +ORDER BY [u].[Id] +"""); + } + + public override async Task Intersect() + { + await base.Intersect(); + + AssertSql( + """ +SELECT i."Id", i."BlogId", i."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + INTERSECT + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" < 22 +) AS i +ORDER BY i."Id" NULLS FIRST +"""); + } + + public override async Task IntersectOnEntitiesWithJson() + { + await base.IntersectOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [i].[Id], [i].[Name], [i].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + INTERSECT + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [i] +ORDER BY [i].[Id] +"""); + } + + public override async Task Except() + { + await base.Except(); + + AssertSql( + """ +SELECT e."Id", e."BlogId", e."Title" +FROM ( + SELECT p."Id", p."BlogId", p."Title" + FROM "Posts" AS p + WHERE p."Id" > 11 + EXCEPT + SELECT p0."Id", p0."BlogId", p0."Title" + FROM "Posts" AS p0 + WHERE p0."Id" > 21 +) AS e +ORDER BY e."Id" NULLS FIRST +"""); + } + + public override async Task ExceptOnEntitiesWithJson() + { + await base.ExceptOnEntitiesWithJson(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Json] +FROM ( + SELECT [b].[Id], [b].[Name], [b].[Json] + FROM [Blogs] AS [b] + WHERE [b].[Id] > 7 + EXCEPT + SELECT [b0].[Id], [b0].[Name], [b0].[Json] + FROM [Blogs] AS [b0] + WHERE [b0].[Id] > 8 +) AS [e] +ORDER BY [e].[Id] +"""); + } + + public override async Task ValuesExpression() + { + await base.ValuesExpression(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE ( + SELECT count(*)::int + FROM (VALUES (7::int), (b."Id")) AS v("Value") + WHERE v."Value" > 8) = 2 +"""); + } + + public override async Task Contains_with_parameterized_collection() + { + await base.Contains_with_parameterized_collection(); + + AssertSql( + """ +@__ids_0={ '1', '2', '3' } (DbType = Object) + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = ANY (@__ids_0) +"""); + } + + public override async Task FromSqlRaw() + { + await base.FromSqlRaw(); + +AssertSql( + """ +SELECT m."Id", m."Name", m."Json" +FROM ( + SELECT * FROM "Blogs" WHERE "Id" > 8 +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + + public override async Task FromSql_with_FormattableString_parameters() + { + await base.FromSql_with_FormattableString_parameters(); + + AssertSql( + """ +p0='8' +p1='9' + +SELECT m."Id", m."Name", m."Json" +FROM ( + SELECT * FROM "Blogs" WHERE "Id" > @p0 AND "Id" < @p1 +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + + #endregion SQL expression quotability + + #region Different query roots + + public override async Task DbContext_as_local_variable() + { + await base.DbContext_as_local_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_field() + { + await base.DbContext_as_field(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_property() + { + await base.DbContext_as_property(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_captured_variable() + { + await base.DbContext_as_captured_variable(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + public override async Task DbContext_as_method_invocation_result() + { + await base.DbContext_as_method_invocation_result(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + #endregion Different query roots + + #region Negative cases + + public override async Task Dynamic_query_does_not_get_precompiled() + { + await base.Dynamic_query_does_not_get_precompiled(); + + AssertSql(); + } + + public override async Task ToList_over_objects_does_not_get_precompiled() + { + await base.ToList_over_objects_does_not_get_precompiled(); + + AssertSql(); + } + + public override async Task Query_compilation_failure() + { + await base.Query_compilation_failure(); + + AssertSql(); + } + + public override async Task EF_Constant_is_not_supported() + { + await base.EF_Constant_is_not_supported(); + + AssertSql(); + } + + public override async Task NotParameterizedAttribute_with_constant() + { + await base.NotParameterizedAttribute_with_constant(); +AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" = 'Blog2' +LIMIT 2 +"""); + } + + public override async Task NotParameterizedAttribute_is_not_supported_with_non_constant_argument() + { + await base.NotParameterizedAttribute_is_not_supported_with_non_constant_argument(); + + AssertSql(); + } + + public override async Task Query_syntax_is_not_supported() + { + await base.Query_syntax_is_not_supported(); + + AssertSql(); + } + + #endregion Negative cases + + public override async Task Select_changes_type() + { + await base.Select_changes_type(); + + AssertSql( + """ +SELECT b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task OrderBy() + { + await base.OrderBy(); + +AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + public override async Task Skip() + { + await base.Skip(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +OFFSET @__p_0 +"""); + } + + public override async Task Take() + { + await base.Take(); + + AssertSql( + """ +@__p_0='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +LIMIT @__p_0 +"""); + } + + public override async Task Project_anonymous_object() + { + await base.Project_anonymous_object(); + + AssertSql( + """ +SELECT COALESCE(b."Name", '') || 'Foo' AS "Foo" +FROM "Blogs" AS b +"""); + } + + public override async Task Two_captured_variables_in_same_lambda() + { + await base.Two_captured_variables_in_same_lambda(); + + AssertSql( + """ +@__yes_0='yes' +@__no_1='no' + +SELECT CASE + WHEN b."Id" = 3 THEN @__yes_0 + ELSE @__no_1 +END +FROM "Blogs" AS b +"""); + } + + public override async Task Two_captured_variables_in_different_lambdas() + { + //Throws because the base startswith is uses a different case "Blog" vs "blog" and postgresql LIKE is case sensitive unlike SQL Server + //Base test fixed upstream for later versions + await Assert.ThrowsAsync(() => base.Two_captured_variables_in_different_lambdas()); + +// AssertSql( +// """ +//@__starts_0_startswith='Blog%' +//@__ends_1_endswith='%2' +// +//SELECT b."Id", b."Name", b."Json" +//FROM "Blogs" AS b +//WHERE b."Name" LIKE @__starts_0_startswith AND b."Name" LIKE @__ends_1_endswith +//LIMIT 2 +//"""); + } + + public override async Task Same_captured_variable_twice_in_same_lambda() + { + await base.Same_captured_variable_twice_in_same_lambda(); + + AssertSql( + """ +@__foo_0_startswith='X%' +@__foo_0_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__foo_0_startswith AND b."Name" LIKE @__foo_0_endswith +"""); + } + + public override async Task Same_captured_variable_twice_in_different_lambdas() + { + await base.Same_captured_variable_twice_in_different_lambdas(); + + AssertSql( + """ +@__foo_0_startswith='X%' +@__foo_0_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @__foo_0_startswith AND b."Name" LIKE @__foo_0_endswith +"""); + } + + public override async Task Include_single() + { + await base.Include_single(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json", p."Id", p."BlogId", p."Title" +FROM "Blogs" AS b +LEFT JOIN "Posts" AS p ON b."Id" = p."BlogId" +WHERE b."Id" > 8 +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Include_split() + { + await base.Include_split(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +""", + // + """ +SELECT p."Id", p."BlogId", p."Title", b."Id" +FROM "Blogs" AS b +INNER JOIN "Posts" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy() + { + await base.Final_GroupBy(); + + AssertSql( + """ +SELECT b."Name", b."Id", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + public override async Task Multiple_queries_with_captured_variables() + { + await base.Multiple_queries_with_captured_variables(); + + AssertSql( + """ +@__id1_0='8' +@__id2_1='9' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 OR b."Id" = @__id2_1 +""", + // + """ +@__id1_0='8' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 +LIMIT 2 +"""); + } + + public override async Task Unsafe_accessor_gets_generated_once_for_multiple_queries() + { + await base.Unsafe_accessor_gets_generated_once_for_multiple_queries(); + + AssertSql( + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +""", + // + """ +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public class PrecompiledQueryNpgsqlFixture : PrecompiledQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } + + protected override async Task SeedAsync(PrecompiledQueryContext context) + { + var blog1 = new Blog { Id = 8, Name = "Blog1", Json = [] }; + var blog2 = new Blog + { + Id = 9, + Name = "Blog2", + Json = + [ + new JsonRoot { Number = 1, Text = "One", Inner = new JsonBranch { Date = new DateTime(2001, 1, 1,0, 0, 0, DateTimeKind.Utc) } }, + new JsonRoot { Number = 2, Text = "Two", Inner = new JsonBranch { Date = new DateTime(2002, 2, 2,0, 0, 0, DateTimeKind.Utc) } }, + ] + }; + + context.Blogs.AddRange(blog1, blog2); + + var post11 = new Post { Id = 11, Title = "Post11", Blog = blog1 }; + var post12 = new Post { Id = 12, Title = "Post12", Blog = blog1 }; + var post21 = new Post { Id = 21, Title = "Post21", Blog = blog2 }; + var post22 = new Post { Id = 22, Title = "Post22", Blog = blog2 }; + var post23 = new Post { Id = 23, Title = "Post23", Blog = blog2 }; + + context.Posts.AddRange(post11, post12, post21, post22, post23); + await context.SaveChangesAsync(); + } + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => NpgsqlPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs new file mode 100644 index 000000000..37958e5b0 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs @@ -0,0 +1,246 @@ +using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +// ReSharper disable InconsistentNaming + +public class PrecompiledSqlPregenerationQueryNpgsqlTest( + PrecompiledSqlPregenerationQueryNpgsqlTest.PrecompiledSqlPregenerationQueryNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledSqlPregenerationQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + public override async Task No_parameters() + { + await base.No_parameters(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = 'foo' +"""); + } + + public override async Task Non_nullable_value_type() + { + await base.Non_nullable_value_type(); + + AssertSql( + """ +@__id_0='8' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id_0 +"""); + } + + public override async Task Nullable_value_type() + { + await base.Nullable_value_type(); + + AssertSql( + """ +@__id_0='8' (Nullable = true) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id_0 +"""); + } + + public override async Task Nullable_reference_type() + { + await base.Nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name_0 +"""); + } + + public override async Task Non_nullable_reference_type() + { + await base.Non_nullable_reference_type(); + + AssertSql( + """ +@__name_0='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name_0 +"""); + } + + public override async Task Nullable_and_non_nullable_value_types() + { + await base.Nullable_and_non_nullable_value_types(); + + AssertSql( + """ +@__id1_0='8' (Nullable = true) +@__id2_1='9' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @__id1_0 OR b."Id" = @__id2_1 +"""); + } + + public override async Task Two_nullable_reference_types() + { + await base.Two_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Two_non_nullable_reference_types() + { + await base.Two_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) +@__name2_1='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Nullable_and_non_nullable_reference_types() + { + await base.Nullable_and_non_nullable_reference_types(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 +"""); + } + + public override async Task Too_many_nullable_parameters_prevent_pregeneration() + { + await base.Too_many_nullable_parameters_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' +@__name2_1='bar' +@__name3_2='baz' +@__name4_3='baq' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 OR b."Name" = @__name3_2 OR b."Name" = @__name4_3 +"""); + } + + public override async Task Many_non_nullable_parameters_do_not_prevent_pregeneration() + { + await base.Many_non_nullable_parameters_do_not_prevent_pregeneration(); + + AssertSql( + """ +@__name1_0='foo' (Nullable = false) +@__name2_1='bar' (Nullable = false) +@__name3_2='baz' (Nullable = false) +@__name4_3='baq' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @__name1_0 OR b."Name" = @__name2_1 OR b."Name" = @__name3_2 OR b."Name" = @__name4_3 +"""); + } + + #region Tests for the different querying enumerables + + public override async Task Include_single_query() + { + await base.Include_single_query(); + + AssertSql( + """ +SELECT b."Id", b."Name", p."Id", p."BlogId", p."Title" +FROM "Blogs" AS b +LEFT JOIN "Post" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Include_split_query() + { + await base.Include_split_query(); + + AssertSql( + """ +SELECT b."Id", b."Name" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +""", + // + """ +SELECT p."Id", p."BlogId", p."Title", b."Id" +FROM "Blogs" AS b +INNER JOIN "Post" AS p ON b."Id" = p."BlogId" +ORDER BY b."Id" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy() + { + await base.Final_GroupBy(); + + AssertSql( + """ +SELECT b."Name", b."Id" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +"""); + } + + #endregion Tests for the different querying enumerables + + public class PrecompiledSqlPregenerationQueryNpgsqlFixture : PrecompiledSqlPregenerationQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + builder = base.AddOptions(builder); + + // TODO: Figure out if there's a nice way to continue using the retrying strategy + var npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder + .ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => NpgsqlPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index ef84acab7..91f9d4e29 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -48,10 +48,12 @@ public override async Task Inline_collection_of_nullable_ints_Contains_null(bool """); } - public override Task Inline_collection_Count_with_zero_values(bool async) - => AssertTranslationFailedWithDetails( - () => base.Inline_collection_Count_with_zero_values(async), - RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + public override async Task Inline_collection_Count_with_zero_values(bool async) + { + await base.Inline_collection_Count_with_zero_values(async); + + AssertSql(); + } public override async Task Inline_collection_Count_with_one_value(bool async) { @@ -98,10 +100,17 @@ SELECT count(*)::int """); } - public override Task Inline_collection_Contains_with_zero_values(bool async) - => AssertTranslationFailedWithDetails( - () => base.Inline_collection_Contains_with_zero_values(async), - RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + public override async Task Inline_collection_Contains_with_zero_values(bool async) + { + await base.Inline_collection_Contains_with_zero_values(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE FALSE +"""); + } public override async Task Inline_collection_Contains_with_one_value(bool async) { @@ -176,6 +185,20 @@ public override async Task Inline_collection_Contains_with_mixed_value_types(boo """ @__i_0='11' +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" IN (999, @__i_0, p."Id", p."Id" + p."Int") +"""); + } + + public override async Task Inline_collection_List_Contains_with_mixed_value_types(bool async) + { + await base.Inline_collection_List_Contains_with_mixed_value_types(async); + + AssertSql( + """ +@__i_0='11' + SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."Int" IN (999, @__i_0, p."Id", p."Id" + p."Int") @@ -206,6 +229,224 @@ public override async Task Inline_collection_negated_Contains_as_All(bool async) """); } + public override async Task Inline_collection_Min_with_two_values(bool async) + { + await base.Inline_collection_Min_with_two_values(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int") = 30 +"""); + } + + public override async Task Inline_collection_List_Min_with_two_values(bool async) + { + await base.Inline_collection_List_Min_with_two_values(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int") = 30 +"""); + } + + public override async Task Inline_collection_Max_with_two_values(bool async) + { + await base.Inline_collection_Max_with_two_values(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int") = 30 +"""); + } + + public override async Task Inline_collection_List_Max_with_two_values(bool async) + { + await base.Inline_collection_List_Max_with_two_values(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int") = 30 +"""); + } + + public override async Task Inline_collection_Min_with_three_values(bool async) + { + await base.Inline_collection_Min_with_three_values(async); + + AssertSql( + """ +@__i_0='25' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @__i_0) = 25 +"""); + } + + public override async Task Inline_collection_List_Min_with_three_values(bool async) + { + await base.Inline_collection_List_Min_with_three_values(async); + + AssertSql( + """ +@__i_0='25' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @__i_0) = 25 +"""); + } + + public override async Task Inline_collection_Max_with_three_values(bool async) + { + await base.Inline_collection_Max_with_three_values(async); + + AssertSql( + """ +@__i_0='35' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @__i_0) = 35 +"""); + } + + public override async Task Inline_collection_List_Max_with_three_values(bool async) + { + await base.Inline_collection_List_Max_with_three_values(async); + + AssertSql( + """ +@__i_0='35' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @__i_0) = 35 +"""); + } + + public override async Task Inline_collection_of_nullable_value_type_Min(bool async) + { + await base.Inline_collection_of_nullable_value_type_Min(async); + + AssertSql( + """ +@__i_0='25' (Nullable = true) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @__i_0) = 25 +"""); + } + + public override async Task Inline_collection_of_nullable_value_type_Max(bool async) + { + await base.Inline_collection_of_nullable_value_type_Max(async); + + AssertSql( + """ +@__i_0='35' (Nullable = true) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @__i_0) = 35 +"""); + } + + public override async Task Inline_collection_of_nullable_value_type_with_null_Min(bool async) + { + await base.Inline_collection_of_nullable_value_type_with_null_Min(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."NullableInt", NULL) = 30 +"""); + } + + public override async Task Inline_collection_of_nullable_value_type_with_null_Max(bool async) + { + await base.Inline_collection_of_nullable_value_type_with_null_Max(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."NullableInt", NULL) = 30 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (@__i_0::int)) AS v("Value") + WHERE v."Value" > p."Id") = 1 +"""); + } + + public override async Task Inline_collection_Contains_with_EF_Parameter(bool async) + { + await base.Inline_collection_Contains_with_EF_Parameter(async); + + AssertSql( + """ +@__p_0={ '2', '999', '1000' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" = ANY (@__p_0) +"""); + } + + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter(bool async) + { + await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(async); + + AssertSql( + """ +@__p_0={ '2', '999', '1000' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(@__p_0) AS p0(value) + WHERE p0.value > p."Id") = 2 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); @@ -223,9 +464,9 @@ FROM unnest(@__ids_0) AS i(value) """); } - public override async Task Parameter_collection_of_ints_Contains(bool async) + public override async Task Parameter_collection_of_ints_Contains_int(bool async) { - await base.Parameter_collection_of_ints_Contains(async); + await base.Parameter_collection_of_ints_Contains_int(async); AssertSql( """ @@ -234,6 +475,100 @@ public override async Task Parameter_collection_of_ints_Contains(bool async) SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."Int" = ANY (@__ints_0) +""", + // + """ +@__ints_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@__ints_0) AND p."Int" = ANY (@__ints_0) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_HashSet_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_HashSet_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@__ints_0) +""", + // + """ +@__ints_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@__ints_0) AND p."Int" = ANY (@__ints_0) IS NOT NULL) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_HashSet_with_value_converter_Contains(bool async) + { + HashSet enums = [MyEnum.Value1, MyEnum.Value4]; + + await AssertQuery( + async, + ss => ss.Set().Where(c => enums.Contains(c.Enum))); + + AssertSql( + """ +@__enums_0={ '0', '3' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Enum" = ANY (@__enums_0) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0={ '10', '999' } (Nullable = false) (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@__ints_0) +""", + // + """ +@__ints_0={ '10', '999' } (Nullable = false) (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@__ints_0) AND p."Int" = ANY (@__ints_0) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) + { + await base.Parameter_collection_of_ints_Contains_nullable_int(async); + + AssertSql( + """ +@__ints_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" = ANY (@__ints_0) OR (p."NullableInt" IS NULL AND array_position(@__ints_0, NULL) IS NOT NULL) +""", + // + """ +@__ints_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableInt" = ANY (@__ints_0) AND p."NullableInt" = ANY (@__ints_0) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@__ints_0, NULL) IS NULL) """); } @@ -248,6 +583,14 @@ public override async Task Parameter_collection_of_nullable_ints_Contains_int(bo SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."Int" = ANY (@__nullableInts_0) +""", + // + """ +@__nullableInts_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@__nullableInts_0) AND p."Int" = ANY (@__nullableInts_0) IS NOT NULL) """); } @@ -262,6 +605,36 @@ public override async Task Parameter_collection_of_nullable_ints_Contains_nullab SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."NullableInt" = ANY (@__nullableInts_0) OR (p."NullableInt" IS NULL AND array_position(@__nullableInts_0, NULL) IS NOT NULL) +""", + // + """ +@__nullableInts_0={ NULL, '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableInt" = ANY (@__nullableInts_0) AND p."NullableInt" = ANY (@__nullableInts_0) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@__nullableInts_0, NULL) IS NULL) +"""); + } + + public override async Task Parameter_collection_of_strings_Contains_string(bool async) + { + await base.Parameter_collection_of_strings_Contains_string(async); + + AssertSql( + """ +@__strings_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."String" = ANY (@__strings_0) +""", + // + """ +@__strings_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."String" = ANY (@__strings_0) AND p."String" = ANY (@__strings_0) IS NOT NULL) """); } @@ -271,25 +644,63 @@ public override async Task Parameter_collection_of_strings_Contains_nullable_str AssertSql( """ -@__strings_0={ '999', NULL } (DbType = Object) +@__strings_0={ '10', '999' } (DbType = Object) SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."NullableString" = ANY (@__strings_0) OR (p."NullableString" IS NULL AND array_position(@__strings_0, NULL) IS NOT NULL) +""", + // + """ +@__strings_0={ '10', '999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableString" = ANY (@__strings_0) AND p."NullableString" = ANY (@__strings_0) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@__strings_0, NULL) IS NULL) """); } - public override async Task Parameter_collection_of_strings_Contains_non_nullable_string(bool async) + public override async Task Parameter_collection_of_nullable_strings_Contains_string(bool async) { - await base.Parameter_collection_of_strings_Contains_non_nullable_string(async); + await base.Parameter_collection_of_nullable_strings_Contains_string(async); AssertSql( """ -@__strings_0={ '10', '999' } (DbType = Object) +@__strings_0={ '10', NULL } (DbType = Object) SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE p."String" = ANY (@__strings_0) +""", + // + """ +@__strings_0={ '10', NULL } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."String" = ANY (@__strings_0) AND p."String" = ANY (@__strings_0) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_nullable_strings_Contains_nullable_string(bool async) + { + await base.Parameter_collection_of_nullable_strings_Contains_nullable_string(async); + + AssertSql( + """ +@__strings_0={ '999', NULL } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableString" = ANY (@__strings_0) OR (p."NullableString" IS NULL AND array_position(@__strings_0, NULL) IS NOT NULL) +""", + // + """ +@__strings_0={ '999', NULL } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableString" = ANY (@__strings_0) AND p."NullableString" = ANY (@__strings_0) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@__strings_0, NULL) IS NULL) """); } @@ -347,6 +758,74 @@ public override async Task Parameter_collection_null_Contains(bool async) """); } + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_Any(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (2), (999), (1000)) AS i("Value") + WHERE i."Value" > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (2), (999), (1000)) AS i("Value") + WHERE i."Value" > p."Id") = 2 +"""); + } + + [ConditionalTheory] // #3012 + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_of_ranges_Contains(bool async) + { + var ranges = new NpgsqlRange[] + { + new(5, 15), + new(40, 50) + }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => ranges.Contains(e.Int)), + ss => ss.Set().Where(c => ranges.Any(p => p.LowerBound <= c.Int && p.UpperBound >= c.Int))); + + AssertSql( + """ +@__ranges_0={ '[5,15]', '[40,50]' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE @__ranges_0 @> p."Int" +"""); + } + public override async Task Column_collection_of_ints_Contains(bool async) { await base.Column_collection_of_ints_Contains(async); @@ -443,6 +922,36 @@ WHERE cardinality(p."Ints") = 2 """); } + public override async Task Column_collection_Count_with_predicate(bool async) + { + await base.Column_collection_Count_with_predicate(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1) = 2 +"""); + } + + public override async Task Column_collection_Where_Count(bool async) + { + await base.Column_collection_Where_Count(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1) = 2 +"""); + } + public override async Task Column_collection_index_int(bool async) { await base.Column_collection_index_int(async); @@ -531,6 +1040,38 @@ ORDER BY v._ord NULLS FIRST """); } + public override async Task Inline_collection_value_index_Column(bool async) + { + await base.Inline_collection_value_index_Column(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT v."Value" + FROM (VALUES (0, 1::int), (1, p."Int"), (2, 3)) AS v(_ord, "Value") + ORDER BY v._ord NULLS FIRST + LIMIT 1 OFFSET p."Int") = 1 +"""); + } + + public override async Task Inline_collection_List_value_index_Column(bool async) + { + await base.Inline_collection_List_value_index_Column(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT v."Value" + FROM (VALUES (0, 1::int), (1, p."Int"), (2, 3)) AS v(_ord, "Value") + ORDER BY v._ord NULLS FIRST + LIMIT 1 OFFSET p."Int") = 1 +"""); + } + public override async Task Parameter_collection_index_Column_equal_Column(bool async) { await base.Parameter_collection_index_Column_equal_Column(async); @@ -571,6 +1112,66 @@ public override async Task Column_collection_ElementAt(bool async) """); } + public override async Task Column_collection_First(bool async) + { + await base.Column_collection_First(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + LIMIT 1) = 1 +"""); + } + + public override async Task Column_collection_FirstOrDefault(bool async) + { + await base.Column_collection_FirstOrDefault(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE COALESCE(( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + LIMIT 1), 0) = 1 +"""); + } + + public override async Task Column_collection_Single(bool async) + { + await base.Column_collection_Single(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + LIMIT 1) = 1 +"""); + } + + public override async Task Column_collection_SingleOrDefault(bool async) + { + await base.Column_collection_SingleOrDefault(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE COALESCE(( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + LIMIT 1), 0) = 1 +"""); + } + public override async Task Column_collection_Skip(bool async) { await base.Column_collection_Skip(async); @@ -604,6 +1205,79 @@ public override async Task Column_collection_Skip_Take(bool async) SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p WHERE 11 = ANY (p."Ints"[2:3]) +"""); + } + + public override async Task Column_collection_Where_Skip(bool async) + { + await base.Column_collection_Where_Skip(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT 1 + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 + OFFSET 1 + ) AS i0) = 3 +"""); + } + + public override async Task Column_collection_Where_Take(bool async) + { + await base.Column_collection_Where_Take(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT 1 + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 + LIMIT 2 + ) AS i0) = 2 +"""); + } + + public override async Task Column_collection_Where_Skip_Take(bool async) + { + await base.Column_collection_Where_Skip_Take(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT 1 + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 + LIMIT 2 OFFSET 1 + ) AS i0) = 1 +"""); + } + + public override async Task Column_collection_Contains_over_subquery(bool async) + { + await base.Column_collection_Contains_over_subquery(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE 11 IN ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 +) """); } @@ -623,6 +1297,22 @@ ORDER BY i.value DESC NULLS LAST """); } + public override async Task Column_collection_Where_ElementAt(bool async) + { + await base.Column_collection_Where_ElementAt(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 + LIMIT 1 OFFSET 0) = 11 +"""); + } + public override async Task Column_collection_Any(bool async) { await base.Column_collection_Any(async); @@ -648,7 +1338,47 @@ SELECT count(*)::int FROM ( SELECT DISTINCT i.value FROM unnest(p."Ints") AS i(value) - ) AS t) = 3 + ) AS i0) = 3 +"""); + } + + public override async Task Column_collection_SelectMany(bool async) + { + await base.Column_collection_SelectMany(async); + + AssertSql( + """ +SELECT i.value +FROM "PrimitiveCollectionsEntity" AS p +JOIN LATERAL unnest(p."Ints") AS i(value) ON TRUE +"""); + } + + public override async Task Column_collection_SelectMany_with_filter(bool async) + { + await base.Column_collection_SelectMany_with_filter(async); + + AssertSql( + """ +SELECT i0.value +FROM "PrimitiveCollectionsEntity" AS p +JOIN LATERAL ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 +) AS i0 ON TRUE +"""); + } + + public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) + { + await base.Column_collection_SelectMany_with_Select_to_anonymous_type(async); + + AssertSql( + """ +SELECT i.value AS "Original", i.value + 1 AS "Incremented" +FROM "PrimitiveCollectionsEntity" AS p +JOIN LATERAL unnest(p."Ints") AS i(value) ON TRUE """); } @@ -710,6 +1440,22 @@ WHERE cardinality(@__ints_0 || p."Ints") = 2 """); } + public override async Task Parameter_collection_with_type_inference_for_JsonScalarExpression(bool async) + { + await base.Parameter_collection_with_type_inference_for_JsonScalarExpression(async); + + AssertSql( + """ +@__values_0={ 'one', 'two' } (DbType = Object) + +SELECT CASE + WHEN p."Id" <> 0 THEN @__values_0[p."Int" % 2 + 1] + ELSE 'foo' +END +FROM "PrimitiveCollectionsEntity" AS p +"""); + } + public override async Task Column_collection_Union_parameter_collection(bool async) { await base.Column_collection_Union_parameter_collection(async); @@ -728,7 +1474,7 @@ FROM unnest(p."Ints") AS i(value) UNION SELECT i0.value FROM unnest(@__ints_0) AS i0(value) - ) AS t) = 2 + ) AS u) = 2 """); } @@ -747,7 +1493,7 @@ SELECT i.value FROM unnest(p."Ints") AS i(value) INTERSECT VALUES (11::int), (111) - ) AS t) = 2 + ) AS i0) = 2 """); } @@ -787,8 +1533,28 @@ SELECT v."Value" EXCEPT SELECT i.value AS "Value" FROM unnest(p."Ints") AS i(value) - ) AS t - WHERE t."Value" % 2 = 1) = 2 + ) AS e + WHERE e."Value" % 2 = 1) = 2 +"""); + } + + public override async Task Column_collection_Where_Union(bool async) + { + await base.Column_collection_Where_Union(async); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 100 + UNION + VALUES (50::int) + ) AS u) = 2 """); } @@ -867,6 +1633,13 @@ await AssertQuery( """); } + public override async Task Column_collection_Where_equality_inline_collection(bool async) + { + await base.Column_collection_Where_equality_inline_collection(async); + + AssertSql(); + } + public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) { await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async); @@ -885,7 +1658,7 @@ FROM unnest(@__ints[2:]) AS i(value) UNION SELECT i0.value FROM unnest(p."Ints") AS i0(value) - ) AS t) = 3 + ) AS u) = 3 """); } @@ -907,7 +1680,7 @@ FROM unnest(@__Skip_0) AS s(value) UNION SELECT i.value FROM unnest(p."Ints") AS i(value) - ) AS t) = 3 + ) AS u) = 3 """); } @@ -927,22 +1700,22 @@ SELECT count(*)::int SELECT s.value FROM unnest(@__Skip_0) AS s(value) UNION - SELECT t1.value + SELECT i2.value FROM ( - SELECT t0.value + SELECT i1.value FROM ( - SELECT DISTINCT t2.value + SELECT DISTINCT i0.value FROM ( - SELECT i.value, i.ordinality - FROM unnest(p."Ints") WITH ORDINALITY AS i(value) + SELECT i.value + FROM unnest(p."Ints") AS i(value) ORDER BY i.value NULLS FIRST OFFSET 1 - ) AS t2 - ) AS t0 - ORDER BY t0.value DESC NULLS LAST + ) AS i0 + ) AS i1 + ORDER BY i1.value DESC NULLS LAST LIMIT 20 - ) AS t1 - ) AS t) = 3 + ) AS i2 + ) AS u) = 3 """); } @@ -995,7 +1768,7 @@ FROM unnest(p."Ints"[2:]) AS i(value) UNION SELECT i0.value FROM unnest(@__ints_0) AS i0(value) - ) AS t) = 3 + ) AS u) = 3 """); } @@ -1030,14 +1803,14 @@ public override async Task Project_collection_of_datetimes_filtered(bool async) AssertSql( """ -SELECT p."Id", t.value, t.ordinality +SELECT p."Id", d0.value, d0.ordinality FROM "PrimitiveCollectionsEntity" AS p LEFT JOIN LATERAL ( SELECT d.value, d.ordinality FROM unnest(p."DateTimes") WITH ORDINALITY AS d(value) - WHERE date_part('day', d.value AT TIME ZONE 'UTC')::int <> 1 OR d.value AT TIME ZONE 'UTC' IS NULL -) AS t ON TRUE -ORDER BY p."Id" NULLS FIRST + WHERE date_part('day', d.value AT TIME ZONE 'UTC')::int <> 1 +) AS d0 ON TRUE +ORDER BY p."Id" NULLS FIRST, d0.ordinality NULLS FIRST """); } @@ -1060,15 +1833,15 @@ public override async Task Project_collection_of_nullable_ints_with_paging2(bool AssertSql( """ -SELECT p."Id", t.value, t.ordinality +SELECT p."Id", n0.value, n0.ordinality FROM "PrimitiveCollectionsEntity" AS p LEFT JOIN LATERAL ( SELECT n.value, n.ordinality FROM unnest(p."NullableInts") WITH ORDINALITY AS n(value) ORDER BY n.value NULLS FIRST OFFSET 1 -) AS t ON TRUE -ORDER BY p."Id" NULLS FIRST, t.value NULLS FIRST +) AS n0 ON TRUE +ORDER BY p."Id" NULLS FIRST, n0.value NULLS FIRST """); } @@ -1091,12 +1864,12 @@ public override async Task Project_collection_of_ints_with_distinct(bool async) AssertSql( """ -SELECT p."Id", t.value +SELECT p."Id", i0.value FROM "PrimitiveCollectionsEntity" AS p LEFT JOIN LATERAL ( SELECT DISTINCT i.value FROM unnest(p."Ints") AS i(value) -) AS t ON TRUE +) AS i0 ON TRUE ORDER BY p."Id" NULLS FIRST """); } @@ -1108,25 +1881,43 @@ public override async Task Project_collection_of_nullable_ints_with_distinct(boo AssertSql(); } + public override async Task Project_collection_of_ints_with_ToList_and_FirstOrDefault(bool async) + { + await base.Project_collection_of_ints_with_ToList_and_FirstOrDefault(async); + + AssertSql( + """ +SELECT p0."Id", i.value, i.ordinality +FROM ( + SELECT p."Id", p."Ints" + FROM "PrimitiveCollectionsEntity" AS p + ORDER BY p."Id" NULLS FIRST + LIMIT 1 +) AS p0 +LEFT JOIN LATERAL unnest(p0."Ints") WITH ORDINALITY AS i(value) ON TRUE +ORDER BY p0."Id" NULLS FIRST, i.ordinality NULLS FIRST +"""); + } + public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) { await base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async); AssertSql( """ -SELECT p."Id", t.value, t.ordinality, t0.value, t0.ordinality +SELECT p."Id", n1.value, n1.ordinality, n2.value, n2.ordinality FROM "PrimitiveCollectionsEntity" AS p LEFT JOIN LATERAL ( SELECT n.value, n.ordinality FROM unnest(p."NullableInts") WITH ORDINALITY AS n(value) WHERE FALSE -) AS t ON TRUE +) AS n1 ON TRUE LEFT JOIN LATERAL ( SELECT n0.value, n0.ordinality FROM unnest(p."NullableInts") WITH ORDINALITY AS n0(value) WHERE n0.value IS NULL -) AS t0 ON TRUE -ORDER BY p."Id" NULLS FIRST, t.ordinality NULLS FIRST +) AS n2 ON TRUE +ORDER BY p."Id" NULLS FIRST, n1.ordinality NULLS FIRST, n2.ordinality NULLS FIRST """); } @@ -1154,21 +1945,21 @@ await AssertQuery( AssertSql( """ -SELECT p."Id", i.value, i.ordinality, i0.value, i0.ordinality, t.value, t.ordinality, t0.value, t0.ordinality +SELECT p."Id", i.value, i.ordinality, i0.value, i0.ordinality, d1.value, d1.ordinality, d2.value, d2.ordinality FROM "PrimitiveCollectionsEntity" AS p LEFT JOIN LATERAL unnest(p."Ints") WITH ORDINALITY AS i(value) ON TRUE LEFT JOIN LATERAL unnest(p."Ints") WITH ORDINALITY AS i0(value) ON TRUE LEFT JOIN LATERAL ( SELECT d.value, d.ordinality FROM unnest(p."DateTimes") WITH ORDINALITY AS d(value) - WHERE date_part('day', d.value AT TIME ZONE 'UTC')::int <> 1 OR d.value AT TIME ZONE 'UTC' IS NULL -) AS t ON TRUE + WHERE date_part('day', d.value AT TIME ZONE 'UTC')::int <> 1 +) AS d1 ON TRUE LEFT JOIN LATERAL ( SELECT d0.value, d0.ordinality FROM unnest(p."DateTimes") WITH ORDINALITY AS d0(value) WHERE d0.value > TIMESTAMPTZ '2000-01-01T00:00:00Z' -) AS t0 ON TRUE -ORDER BY p."Id" NULLS FIRST, i.ordinality NULLS FIRST, i0.value DESC NULLS LAST, i0.ordinality NULLS FIRST, t.ordinality NULLS FIRST +) AS d2 ON TRUE +ORDER BY p."Id" NULLS FIRST, i.ordinality NULLS FIRST, i0.value DESC NULLS LAST, i0.ordinality NULLS FIRST, d1.ordinality NULLS FIRST, d2.ordinality NULLS FIRST """); } @@ -1185,6 +1976,96 @@ ORDER BY p."Id" NULLS FIRST """); } + public override async Task Project_inline_collection(bool async) + { + await base.Project_inline_collection(async); + + AssertSql( + """ +SELECT ARRAY[p."String",'foo']::text[] +FROM "PrimitiveCollectionsEntity" AS p +"""); + } + + public override async Task Project_inline_collection_with_Union(bool async) + { + await base.Project_inline_collection_with_Union(async); + + AssertSql( + """ +SELECT p."Id", u."Value" +FROM "PrimitiveCollectionsEntity" AS p +LEFT JOIN LATERAL ( + SELECT v."Value" + FROM (VALUES (p."String")) AS v("Value") + UNION + SELECT p0."String" AS "Value" + FROM "PrimitiveCollectionsEntity" AS p0 +) AS u ON TRUE +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Project_inline_collection_with_Concat(bool async) + { + await base.Project_inline_collection_with_Concat(async); + + AssertSql(); + } + + public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(async); + + AssertSql( + """ +@__ints_0={ '1', '2', '3' } (DbType = Object) +@__strings_1={ 'one', 'two', 'three' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE CASE + WHEN p."Int" = ANY (@__ints_0) THEN 'one' + ELSE 'two' +END = ANY (@__strings_1) +"""); + } + + public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(async); + + AssertSql( + """ +@__ints_0={ '1', '2', '3' } (DbType = Object) +@__strings_1={ 'one', 'two', 'three' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE CASE + WHEN p."Int" = ANY (@__ints_0) THEN 'one' + ELSE 'two' +END = ANY (@__strings_1) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_remove(bool async) + { + await AssertQuery( + async, + // ReSharper disable once ReplaceWithSingleCallToCount + ss => ss.Set().Where(e => e.Ints.Where(i => i != 1).Count() == 1)); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(array_remove(p."Ints", 1)) = 1 +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); @@ -1195,7 +2076,7 @@ private void AssertSql(params string[] expected) private PrimitiveCollectionsContext CreateContext() => Fixture.CreateContext(); - public class PrimitiveCollectionsQueryNpgsqlFixture : PrimitiveCollectionsQueryFixtureBase + public class PrimitiveCollectionsQueryNpgsqlFixture : PrimitiveCollectionsQueryFixtureBase, ITestSqlLoggerFactory { public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs b/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs index 861b233b8..3dd78fa0d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs @@ -20,16 +20,16 @@ public QueryBugsTest(NpgsqlFixture fixture, ITestOutputHelper testOutputHelper) #region Bug920 [Fact] - public void Bug920() + public async Task Bug920() { - using var _ = CreateDatabase920(); + await using var _ = await CreateDatabase920Async(); using var context = new Bug920Context(_options); context.Entities.Add(new Bug920Entity { Enum = Bug920Enum.Two }); context.SaveChanges(); } - private NpgsqlTestStore CreateDatabase920() - => CreateTestStore(() => new Bug920Context(_options), _ => ClearLog()); + private Task CreateDatabase920Async() + => CreateTestStoreAsync(() => new Bug920Context(_options), _ => ClearLog()); public enum Bug920Enum { One, Two } @@ -41,13 +41,8 @@ public class Bug920Entity public Bug920Enum Enum { get; set; } } - private class Bug920Context : DbContext + private class Bug920Context(DbContextOptions options) : DbContext(options) { - public Bug920Context(DbContextOptions options) - : base(options) - { - } - public DbSet Entities { get; set; } } @@ -55,17 +50,17 @@ public Bug920Context(DbContextOptions options) private DbContextOptions _options; - private NpgsqlTestStore CreateTestStore( + private async Task CreateTestStoreAsync( Func contextCreator, Action contextInitializer) where TContext : DbContext, IDisposable { - var testStore = NpgsqlTestStore.CreateInitialized("QueryBugsTest"); + var testStore = await NpgsqlTestStore.CreateInitializedAsync("QueryBugsTest"); _options = Fixture.CreateOptions(testStore); - using var context = contextCreator(); - context.Database.EnsureCreatedResiliently(); + await using var context = contextCreator(); + await context.Database.EnsureCreatedResilientlyAsync(); contextInitializer?.Invoke(context); return testStore; } diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs index cafe75bdb..4c820b3ef 100644 --- a/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs @@ -22,15 +22,15 @@ public override void DbContext_list_is_parameterized() // when the list is null. We translate to server-side with PostgresAnyExpression, so no exception is thrown. // Assert.Throws(() => context.Set().ToList()); - context.TenantIds = new List(); + context.TenantIds = []; var query = context.Set().ToList(); Assert.Empty(query); - context.TenantIds = new List { 1 }; + context.TenantIds = [1]; query = context.Set().ToList(); Assert.Single(query); - context.TenantIds = new List { 2, 3 }; + context.TenantIds = [2, 3]; query = context.Set().ToList(); Assert.Equal(2, query.Count); } diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs index ddfec4bfe..1e31adfbd 100644 --- a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs @@ -1,5 +1,3 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class QueryNoClientEvalNpgsqlFixture : NorthwindQueryNpgsqlFixture -{ -} +public class QueryNoClientEvalNpgsqlFixture : NorthwindQueryNpgsqlFixture; diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs index 15816405a..d33862cdc 100644 --- a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs @@ -1,9 +1,4 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class QueryNoClientEvalNpgsqlTest : QueryNoClientEvalTestBase -{ - public QueryNoClientEvalNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class QueryNoClientEvalNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) + : QueryNoClientEvalTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs index 3e7000cf9..29772f60e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs @@ -259,17 +259,17 @@ public void Union_aggregate() .Select(g => g.Select(x => x.IntRange).RangeAgg()) .Single(); - Assert.Equal(new NpgsqlRange[] { new(1, true, 16, false) }, union); + Assert.Equal([new(1, true, 16, false)], union); AssertSql( """ -SELECT range_agg(t."IntRange") +SELECT range_agg(r0."IntRange") FROM ( SELECT r."IntRange", TRUE AS "Key" FROM "RangeTestEntities" AS r WHERE r."Id" IN (1, 2) -) AS t -GROUP BY t."Key" +) AS r0 +GROUP BY r0."Key" LIMIT 2 """); } @@ -309,13 +309,13 @@ public void Intersect_aggregate() AssertSql( """ -SELECT range_intersect_agg(t."IntRange") +SELECT range_intersect_agg(r0."IntRange") FROM ( SELECT r."IntRange", TRUE AS "Key" FROM "RangeTestEntities" AS r WHERE r."Id" IN (1, 2) -) AS t -GROUP BY t."Key" +) AS r0 +GROUP BY r0."Key" LIMIT 2 """); } @@ -626,8 +626,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(RangeContext context) - => RangeContext.Seed(context); + protected override Task SeedAsync(RangeContext context) + => RangeContext.SeedAsync(context); public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { @@ -654,20 +654,15 @@ public class RangeTestEntity public NpgsqlRange UserDefinedRangeWithSchema { get; set; } } - public class RangeContext : PoolableDbContext + public class RangeContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet RangeTestEntities { get; set; } - public RangeContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder builder) => builder.HasPostgresRange("doublerange", "double precision") .HasPostgresRange("test", "Schema_Range", "real"); - public static void Seed(RangeContext context) + public static async Task SeedAsync(RangeContext context) { context.RangeTestEntities.AddRange( new RangeTestEntity @@ -693,7 +688,7 @@ public static void Seed(RangeContext context) UserDefinedRangeWithSchema = new NpgsqlRange(5, 15) }); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs index 6fa1f5c1b..91ef5653a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs @@ -9,9 +9,4 @@ protected override ITestStoreFactory TestStoreFactory public override Task Can_use_shared_type_entity_type_in_query_filter_with_from_sql(bool async) => Task.CompletedTask; // https://github.com/dotnet/efcore/issues/25661 - - [ConditionalFact(Skip = "https://github.com/dotnet/efcore/issues/30367")] - public override void Ad_hoc_query_for_shared_type_entity_type_works() - { - } } 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/SpatialQueryNpgsqlGeographyTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs index 86d0c9b83..8508607b7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs @@ -22,10 +22,10 @@ public SpatialQueryNpgsqlGeographyTest(SpatialQueryNpgsqlGeographyFixture fixtur protected override bool AssertDistances => false; - public static IEnumerable IsAsyncDataAndUseSpheroid = new[] - { - new object[] { false, false }, new object[] { false, true }, new object[] { true, false }, new object[] { true, true } - }; + public static IEnumerable IsAsyncDataAndUseSpheroid = + [ + [false, false], [false, true], [true, false], [true, true] + ]; public override async Task Area(bool async) { diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs index 8e156866b..6bf97811b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -740,7 +740,7 @@ public override async Task Z(bool async) [MemberData(nameof(IsAsyncData))] public virtual async Task MultiString_Any(bool async) { - var lineString = Fixture.GeometryFactory.CreateLineString(new[] { new Coordinate(1, 0), new Coordinate(1, 1) }); + var lineString = Fixture.GeometryFactory.CreateLineString([new Coordinate(1, 0), new Coordinate(1, 1)]); // Note the subtle difference between Contains and Any here: Contains resolves to Geometry.Contains, which checks whether a geometry // is contained in another; this is different from .NET collection/enumerable Contains, which checks whether an item is in a diff --git a/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs index 7bd5ec4d1..d761b5586 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs @@ -2,22 +2,24 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class SqlExecutorNpgsqlTest : SqlExecutorTestBase> +public class SqlExecutorNpgsqlTest : SqlExecutorTestBase> { - public SqlExecutorNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) + public SqlExecutorNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override DbParameter CreateDbParameter(string name, object value) => new NpgsqlParameter { ParameterName = name, Value = value }; protected override string TenMostExpensiveProductsSproc - => @"SELECT * FROM ""Ten Most Expensive Products""()"; + => """SELECT * FROM "Ten Most Expensive Products"()"""; protected override string CustomerOrderHistorySproc - => @"SELECT * FROM ""CustOrderHist""(@CustomerID)"; + => """SELECT * FROM "CustOrderHist"(@CustomerID)"""; protected override string CustomerOrderHistoryWithGeneratedParameterSproc - => @"SELECT * FROM ""CustOrderHist""({0})"; + => """SELECT * FROM "CustOrderHist"({0})"""; } diff --git a/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs index 20f43c4e8..d666a26f1 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs @@ -1,4 +1,5 @@ using System.Data.Common; +using Xunit.Sdk; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; @@ -40,6 +41,12 @@ public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and """); } + // The test attempts to project out a column with the wrong case; this works on other databases, and fails when EF tries to materialize. + // But in PG this fails at the database since PG is case-sensitive and the column does not exist. + public override Task SqlQueryRaw_queryable_simple_different_cased_columns_and_not_enough_columns_throws(bool async) + => Assert.ThrowsAsync( + () => base.SqlQueryRaw_queryable_simple_different_cased_columns_and_not_enough_columns_throws(async)); + public override async Task SqlQueryRaw_queryable_composed(bool async) { await base.SqlQueryRaw_queryable_composed(async); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs index bb7821813..ac1e24fcb 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs @@ -1,9 +1,4 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class TPCFiltersInheritanceQueryNpgsqlTest : TPCFiltersInheritanceQueryTestBase -{ - public TPCFiltersInheritanceQueryNpgsqlTest(TPCFiltersInheritanceQueryNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class TPCFiltersInheritanceQueryNpgsqlTest(TPCFiltersInheritanceQueryNpgsqlFixture fixture) + : TPCFiltersInheritanceQueryTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs index b0703bb02..a6510bd5e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs @@ -46,12 +46,12 @@ public override ISetSource GetExpectedData() return _expectedData; } - protected override void Seed(GearsOfWarContext context) + protected override Task SeedAsync(GearsOfWarContext context) // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. - => SeedForNpgsql(context); + => SeedForNpgsqlAsync(context); - public static void SeedForNpgsql(GearsOfWarContext context) + public static async Task SeedForNpgsqlAsync(GearsOfWarContext context) { var squads = GearsOfWarData.CreateSquads(); var missions = GearsOfWarData.CreateMissions(); @@ -85,10 +85,10 @@ public static void SeedForNpgsql(GearsOfWarContext context) context.LocustLeaders.AddRange(locustLeaders); context.Factions.AddRange(factions); context.LocustHighCommands.AddRange(locustHighCommands); - context.SaveChanges(); + await context.SaveChangesAsync(); GearsOfWarData.WireUp2(locustLeaders, factions); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs index 5f047dd73..5fae337e4 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs @@ -48,7 +48,7 @@ await AssertQuery( @__end_1='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) @__dates_2={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE @__start_0 <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @__end_1 AND m."Timeline" = ANY (@__dates_2) """); @@ -66,7 +66,7 @@ await AssertQuery( """ @__dateTimeOffset_Date_0='0002-03-01T00:00:00.0000000' -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamp >= @__dateTimeOffset_Date_0 """); @@ -82,7 +82,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs index 3ce9b2085..6804a75a8 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs @@ -1,12 +1,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class TPCInheritanceQueryNpgsqlTest : TPCInheritanceQueryTestBase +public class TPCInheritanceQueryNpgsqlTest(TPCInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : TPCInheritanceQueryTestBase(fixture, testOutputHelper) { - public TPCInheritanceQueryNpgsqlTest(TPCInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); } diff --git a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs index 7957eb5a1..eedc4427e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs @@ -12,6 +12,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity().HasNoKey().ToSqlQuery(@"SELECT * FROM ""Animals"""); + modelBuilder.Entity().HasNoKey().ToSqlQuery(""" + SELECT * FROM "Animals" + """); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlTest.cs index 066e3e549..1eb085607 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlTest.cs @@ -1,9 +1,4 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class TPHInheritanceQueryNpgsqlTest : TPHInheritanceQueryTestBase -{ - public TPHInheritanceQueryNpgsqlTest(TPHInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } -} +public class TPHInheritanceQueryNpgsqlTest(TPHInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : TPHInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs index a6dce923f..68ca376e4 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs @@ -46,12 +46,12 @@ public override ISetSource GetExpectedData() return _expectedData; } - protected override void Seed(GearsOfWarContext context) + protected override Task SeedAsync(GearsOfWarContext context) // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. // Also chop sub-microsecond precision which PostgreSQL does not support. - => SeedForNpgsql(context); + => SeedForNpgsqlAsync(context); - public static void SeedForNpgsql(GearsOfWarContext context) + public static async Task SeedForNpgsqlAsync(GearsOfWarContext context) { var squads = GearsOfWarData.CreateSquads(); var missions = GearsOfWarData.CreateMissions(); @@ -85,10 +85,10 @@ public static void SeedForNpgsql(GearsOfWarContext context) context.LocustLeaders.AddRange(locustLeaders); context.Factions.AddRange(factions); context.LocustHighCommands.AddRange(locustHighCommands); - context.SaveChanges(); + await context.SaveChangesAsync(); GearsOfWarData.WireUp2(locustLeaders, factions); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs index 9247f53db..dafbe74e1 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs @@ -52,7 +52,7 @@ await AssertQuery( @__end_1='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) @__dates_2={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE @__start_0 <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @__end_1 AND m."Timeline" = ANY (@__dates_2) """); @@ -70,7 +70,7 @@ await AssertQuery( """ @__dateTimeOffset_Date_0='0002-03-01T00:00:00.0000000' -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamp >= @__dateTimeOffset_Date_0 """); @@ -86,7 +86,7 @@ await AssertQuery( AssertSql( """ -SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs index 5359e75f7..8ddc46103 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs @@ -1,10 +1,4 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class TPTInheritanceQueryNpgsqlTest : TPTInheritanceQueryTestBase -{ - // ReSharper disable once UnusedParameter.Local - public TPTInheritanceQueryNpgsqlTest(TPTInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture, testOutputHelper) - { - } -} +public class TPTInheritanceQueryNpgsqlTest(TPTInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : TPTInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs index 28ab41a48..7b6e17486 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs @@ -744,6 +744,56 @@ WHERE CAST(e."TimestamptzDateTime" AT TIME ZONE 'UTC' AS date) + TIME '15:26:38' """); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task DateOnly_DayNumber(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => DateOnly.FromDateTime(e.TimestamptzDateTime).DayNumber == 729490)); + + AssertSql( + """ +SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" +FROM "Entities" AS e +WHERE CAST(e."TimestamptzDateTime" AT TIME ZONE 'UTC' AS date) - DATE '0001-01-01' = 729490 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task DateOnly_DayNumber_subtraction(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + e => DateOnly.FromDateTime(e.TimestamptzDateTime).DayNumber - + DateOnly.FromDateTime(e.TimestamptzDateTime - TimeSpan.FromDays(3)).DayNumber == 3)); + + AssertSql( + """ +SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" +FROM "Entities" AS e +WHERE CAST(e."TimestamptzDateTime" AT TIME ZONE 'UTC' AS date) - CAST((e."TimestamptzDateTime" - INTERVAL '3 00:00:00') AT TIME ZONE 'UTC' AS date) = 3 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task DateOnly_FromDayNumber(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => DateOnly.FromDayNumber(e.Id) == new DateOnly(0001, 01, 03))); + + AssertSql( + """ +SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" +FROM "Entities" AS e +WHERE DATE '0001-01-01' + e."Id" = DATE '0001-01-03' +"""); + } + #endregion DateOnly #region TimeOnly @@ -825,19 +875,14 @@ private TimestampQueryContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class TimestampQueryContext : PoolableDbContext + public class TimestampQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet Entities { get; set; } - public TimestampQueryContext(DbContextOptions options) - : base(options) - { - } - - public static void Seed(TimestampQueryContext context) + public static async Task SeedAsync (TimestampQueryContext context) { context.Entities.AddRange(TimestampData.CreateEntities()); - context.SaveChanges(); + await context.SaveChangesAsync(); } } @@ -865,7 +910,7 @@ public class Entity public NpgsqlRange TimestampDateTimeRange { get; set; } } - public class TimestampQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase + public class TimestampQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory { protected override string StoreName => "TimestampQueryTest"; @@ -874,15 +919,15 @@ 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; private TimestampData _expectedData; - protected override void Seed(TimestampQueryContext context) - => TimestampQueryContext.Seed(context); + protected override Task SeedAsync(TimestampQueryContext context) + => TimestampQueryContext.SeedAsync(context); public Func GetContextCreator() => CreateContext; @@ -926,12 +971,7 @@ public IReadOnlyDictionary EntityAsserters protected class TimestampData : ISetSource { - public IReadOnlyList Entities { get; } - - public TimestampData() - { - Entities = CreateEntities(); - } + public IReadOnlyList Entities { get; } = CreateEntities(); public IQueryable Set() where TEntity : class @@ -988,7 +1028,7 @@ public static IReadOnlyList CreateEntities() TimestampDateTimeOffset = new DateTimeOffset(utcDateTime1), TimestamptzDateTimeArray = utcDateTimeArray1, TimestampDateTimeArray = localDateTimeArray1, - TimestampDateTimeOffsetArray = new DateTimeOffset[] { new(utcDateTimeArray1[0]), new(utcDateTimeArray1[1]) }, + TimestampDateTimeOffsetArray = [new(utcDateTimeArray1[0]), new(utcDateTimeArray1[1])], TimestamptzDateTimeRange = utcDateTimeRange1, TimestampDateTimeRange = localDateTimeRange1, }, @@ -1000,7 +1040,7 @@ public static IReadOnlyList CreateEntities() TimestampDateTimeOffset = new DateTimeOffset(utcDateTime2), TimestamptzDateTimeArray = utcDateTimeArray2, TimestampDateTimeArray = localDateTimeArray2, - TimestampDateTimeOffsetArray = new DateTimeOffset[] { new(utcDateTimeArray2[0]), new(utcDateTimeArray2[1]) }, + TimestampDateTimeOffsetArray = [new(utcDateTimeArray2[0]), new(utcDateTimeArray2[1])], TimestamptzDateTimeRange = utcDateTimeRange2, TimestampDateTimeRange = localDateTimeRange2, } diff --git a/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs index e9b5e1d85..12f3668e0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs @@ -27,33 +27,33 @@ public TrigramsQueryNpgsqlTest(TrigramsQueryNpgsqlFixture fixture, ITestOutputHe public void TrigramsShow() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsShow(x.Text)) .ToArray(); - AssertContainsSql(@"show_trgm(t.""Text"")"); + AssertContainsSql("""show_trgm(t."Text")"""); } [Fact] public void TrigramsSimilarity() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsSimilarity(x.Text, "target")) .ToArray(); - AssertContainsSql(@"similarity(t.""Text"", 'target')"); + AssertContainsSql("""similarity(t."Text", 'target')"""); } [Fact] public void TrigramsWordSimilarity() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsWordSimilarity(x.Text, "target")) .ToArray(); - AssertContainsSql(@"word_similarity(t.""Text"", 'target')"); + AssertContainsSql("""word_similarity(t."Text", 'target')"""); } [ConditionalFact] @@ -61,44 +61,44 @@ public void TrigramsWordSimilarity() public void TrigramsStrictWordSimilarity() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsStrictWordSimilarity(x.Text, "target")) .ToArray(); - AssertContainsSql(@"strict_word_similarity(t.""Text"", 'target')"); + AssertContainsSql("""strict_word_similarity(t."Text", 'target')"""); } [Fact] public void TrigramsAreSimilar() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsAreSimilar(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" % 'target'"); + AssertContainsSql("""t."Text" % 'target'"""); } [Fact] public void TrigramsAreWordSimilar() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsAreWordSimilar(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <% 'target'"); + AssertContainsSql("""t."Text" <% 'target'"""); } [Fact] public void TrigramsAreNotWordSimilar() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsAreNotWordSimilar(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" %> 'target'"); + AssertContainsSql("""t."Text" %> 'target'"""); } [ConditionalFact] @@ -106,11 +106,11 @@ public void TrigramsAreNotWordSimilar() public void TrigramsAreStrictWordSimilar() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsAreStrictWordSimilar(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <<% 'target'"); + AssertContainsSql("""t."Text" <<% 'target'"""); } [ConditionalFact] @@ -118,44 +118,44 @@ public void TrigramsAreStrictWordSimilar() public void TrigramsAreNotStrictWordSimilar() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsAreNotStrictWordSimilar(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" %>> 'target'"); + AssertContainsSql("""t."Text" %>> 'target'"""); } [Fact] public void TrigramsSimilarityDistance() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsSimilarityDistance(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <-> 'target'"); + AssertContainsSql("""t."Text" <-> 'target'"""); } [Fact] public void TrigramsWordSimilarityDistance() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsWordSimilarityDistance(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <<-> 'target'"); + AssertContainsSql("""t."Text" <<-> 'target'"""); } [Fact] public void TrigramsWordSimilarityDistanceInverted() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsWordSimilarityDistanceInverted(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <->> 'target'"); + AssertContainsSql("""t."Text" <->> 'target'"""); } [ConditionalFact] @@ -163,11 +163,11 @@ public void TrigramsWordSimilarityDistanceInverted() public void TrigramsStrictWordSimilarityDistance() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsStrictWordSimilarityDistance(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <<<-> 'target'"); + AssertContainsSql("""t."Text" <<<-> 'target'"""); } [ConditionalFact] @@ -175,18 +175,18 @@ public void TrigramsStrictWordSimilarityDistance() public void TrigramsStrictWordSimilarityDistanceInverted() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Select(x => EF.Functions.TrigramsStrictWordSimilarityDistanceInverted(x.Text, "target")) .ToArray(); - AssertContainsSql(@"t.""Text"" <->>> 'target'"); + AssertContainsSql("""t."Text" <->>> 'target'"""); } [Fact] // #1659 - public void Operator_precedence() + public void Concatenation_operator_precedence() { using var context = CreateContext(); - var _ = context.TrigramsTestEntities + _ = context.TrigramsTestEntities .Where(e => EF.Functions.TrigramsAreSimilar(e.Text + " " + e.Text, "query")) .ToArray(); @@ -198,6 +198,22 @@ public void Operator_precedence() """); } + [Fact] // #3072 + public void PgUnknownBinary_operator_precedence() + { + using var context = CreateContext(); + _ = context.TrigramsTestEntities + .Where(e => 1 - EF.Functions.TrigramsSimilarityDistance(e.Text, "query") > 8) + .ToArray(); + + AssertSql( + """ +SELECT t."Id", t."Text" +FROM "TrigramsTestEntities" AS t +WHERE 1 - (t."Text" <-> 'query') > 8 +"""); + } + #endregion #region Fixtures @@ -216,8 +232,8 @@ protected override ITestStoreFactory TestStoreFactory public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - protected override void Seed(TrigramsContext context) - => TrigramsContext.Seed(context); + protected override Task SeedAsync(TrigramsContext context) + => TrigramsContext.SeedAsync(context); } /// @@ -266,7 +282,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - public static void Seed(TrigramsContext context) + public static async Task SeedAsync(TrigramsContext context) { for (var i = 1; i <= 9; i++) { @@ -275,7 +291,7 @@ public static void Seed(TrigramsContext context) new TrigramsTestEntity { Id = i, Text = text }); } - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs b/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs index fc1144d10..549194272 100644 --- a/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs +++ b/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs @@ -232,12 +232,12 @@ public override void Scalar_Function_Let_Nested_Static() AssertSql( """ -@__starCount_0='3' -@__customerId_1='1' +@__starCount_1='3' +@__customerId_0='1' -SELECT c."LastName", "StarValue"(@__starCount_0, "CustomerOrderCount"(@__customerId_1)) AS "OrderCount" +SELECT c."LastName", "StarValue"(@__starCount_1, "CustomerOrderCount"(@__customerId_0)) AS "OrderCount" FROM "Customers" AS c -WHERE c."Id" = @__customerId_1 +WHERE c."Id" = @__customerId_0 LIMIT 2 """); } @@ -530,12 +530,12 @@ public override void Scalar_Function_Let_Nested_Instance() AssertSql( """ -@__starCount_1='3' -@__customerId_2='1' +@__starCount_2='3' +@__customerId_1='1' -SELECT c."LastName", "StarValue"(@__starCount_1, "CustomerOrderCount"(@__customerId_2)) AS "OrderCount" +SELECT c."LastName", "StarValue"(@__starCount_2, "CustomerOrderCount"(@__customerId_1)) AS "OrderCount" FROM "Customers" AS c -WHERE c."Id" = @__customerId_2 +WHERE c."Id" = @__customerId_1 LIMIT 2 """); } @@ -588,13 +588,8 @@ LIMIT 2 public override void Scalar_Function_with_nullable_value_return_type_throws() {} #endif - protected class NpgsqlUDFSqlContext : UDFSqlContext + protected class NpgsqlUDFSqlContext(DbContextOptions options) : UDFSqlContext(options) { - public NpgsqlUDFSqlContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -676,11 +671,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .HasParameter("startDate").Metadata.TypeMapping = typeMappingSource.GetMapping("timestamp without time zone"); } - protected override void Seed(DbContext context) + protected override async Task SeedAsync(DbContext context) { - base.Seed(context); + await base.SeedAsync(context); - context.Database.ExecuteSqlRaw( + await context.Database.ExecuteSqlRawAsync( """ CREATE FUNCTION "CustomerOrderCount" ("customerId" INTEGER) RETURNS INTEGER AS $$ SELECT COUNT("Id")::INTEGER FROM "Orders" WHERE "CustomerId" = $1 $$ @@ -781,7 +776,7 @@ EXCEPTION WHEN OTHERS THEN $$ LANGUAGE PLPGSQL; """); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs index 3211f7d17..2e10b22e0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs @@ -1,9 +1,3 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; -public class WarningsNpgsqlTest : WarningsTestBase -{ - public WarningsNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class WarningsNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) : WarningsTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs b/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs index 320f4b63b..8ce7a886d 100644 --- a/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs +++ b/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs @@ -4,13 +4,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public abstract class QueryExpressionInterceptionNpgsqlTestBase : QueryExpressionInterceptionTestBase +public abstract class QueryExpressionInterceptionNpgsqlTestBase( + QueryExpressionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) + : QueryExpressionInterceptionTestBase(fixture) { - protected QueryExpressionInterceptionNpgsqlTestBase(InterceptionNpgsqlFixtureBase fixture) - : base(fixture) - { - } - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase { protected override ITestStoreFactory TestStoreFactory @@ -29,14 +26,9 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build } } - public class QueryExpressionInterceptionNpgsqlTest - : QueryExpressionInterceptionNpgsqlTestBase, IClassFixture + public class QueryExpressionInterceptionNpgsqlTest(QueryExpressionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) + : QueryExpressionInterceptionNpgsqlTestBase(fixture), IClassFixture { - public QueryExpressionInterceptionNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override string StoreName @@ -47,15 +39,11 @@ protected override bool ShouldSubscribeToDiagnosticListener } } - public class QueryExpressionInterceptionWithDiagnosticsNpgsqlTest - : QueryExpressionInterceptionNpgsqlTestBase, + public class QueryExpressionInterceptionWithDiagnosticsNpgsqlTest( + QueryExpressionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) + : QueryExpressionInterceptionNpgsqlTestBase(fixture), IClassFixture { - public QueryExpressionInterceptionWithDiagnosticsNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override string StoreName diff --git a/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs index b0ff63af5..c62c896b9 100644 --- a/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public abstract class SaveChangesInterceptionNpgsqlTestBase : SaveChangesInterceptionTestBase +public abstract class SaveChangesInterceptionNpgsqlTestBase(SaveChangesInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) + : SaveChangesInterceptionTestBase(fixture) { - protected SaveChangesInterceptionNpgsqlTestBase(InterceptionNpgsqlFixtureBase fixture) - : base(fixture) - { - } - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase { protected override ITestStoreFactory TestStoreFactory @@ -29,14 +25,9 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build } } - public class SaveChangesInterceptionNpgsqlTest - : SaveChangesInterceptionNpgsqlTestBase, IClassFixture + public class SaveChangesInterceptionNpgsqlTest(SaveChangesInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) + : SaveChangesInterceptionNpgsqlTestBase(fixture), IClassFixture { - public SaveChangesInterceptionNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override string StoreName @@ -54,15 +45,11 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build } } - public class SaveChangesInterceptionWithDiagnosticsNpgsqlTest - : SaveChangesInterceptionNpgsqlTestBase, + public class SaveChangesInterceptionWithDiagnosticsNpgsqlTest( + SaveChangesInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) + : SaveChangesInterceptionNpgsqlTestBase(fixture), IClassFixture { - public SaveChangesInterceptionWithDiagnosticsNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override string StoreName diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs new file mode 100644 index 000000000..f2ad5e6ac --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs @@ -0,0 +1,59 @@ +// using System.Runtime.CompilerServices; +// using Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; +// using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +// using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +// +// namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding; +// +// public class CompiledModelNpgsqlTest : CompiledModelRelationalTestBase +// { +// protected override TestHelpers TestHelpers => NpgsqlTestHelpers.Instance; +// protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; +// +// // #3087 +// public override void BigModel() +// => Assert.Throws(() => base.BigModel()); +// +// // #3087 +// public override void BigModel_with_JSON_columns() +// => Assert.Throws(() => base.BigModel()); +// +// // #3087 +// public override void CheckConstraints() +// => Assert.Throws(() => base.BigModel()); +// +// // #3087 +// public override void DbFunctions() +// => Assert.Throws(() => base.BigModel()); +// +// // #3087 +// public override void Triggers() +// => Assert.Throws(() => base.BigModel()); +// +// // https://github.com/dotnet/efcore/pull/32341/files#r1485603038 +// public override void Tpc() +// => Assert.Throws(() => base.Tpc()); +// +// // https://github.com/dotnet/efcore/pull/32341/files#r1485603038 +// public override void ComplexTypes() +// => Assert.Throws(() => base.ComplexTypes()); +// +// protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) +// { +// new NpgsqlDbContextOptionsBuilder(builder).UseNetTopologySuite(); +// return builder; +// } +// +// protected override void AddDesignTimeServices(IServiceCollection services) +// => new NpgsqlNetTopologySuiteDesignTimeServices().ConfigureDesignTimeServices(services); +// +// protected override BuildSource AddReferences(BuildSource build, [CallerFilePath] string filePath = "") +// { +// base.AddReferences(build); +// build.References.Add(BuildReference.ByName("Npgsql.EntityFrameworkCore.PostgreSQL")); +// build.References.Add(BuildReference.ByName("Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite")); +// build.References.Add(BuildReference.ByName("Npgsql")); +// build.References.Add(BuildReference.ByName("NetTopologySuite")); +// return build; +// } +// } diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs index 06b83238d..eb0179a68 100644 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs @@ -42,8 +42,8 @@ MAXVALUE 8 MINVALUE -3 CYCLE; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var defaultSequence = dbModel.Sequences.First(ds => ds.Name == "DefaultFacetsSequence"); @@ -81,8 +81,8 @@ public void Sequence_min_max_start_values_are_null_if_default() CREATE SEQUENCE "IntSequence" AS int; CREATE SEQUENCE "BigIntSequence" AS bigint; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { Assert.All( @@ -107,8 +107,8 @@ public void Filter_sequences_based_on_schema() CREATE SEQUENCE "Sequence"; CREATE SEQUENCE db2."Sequence" """, - Enumerable.Empty(), - new[] { "db2" }, + [], + ["db2"], dbModel => { var sequence = Assert.Single(dbModel.Sequences); @@ -130,8 +130,8 @@ CREATE SEQUENCE db2."Sequence" public void Set_default_schema() => Test( "SELECT 1", - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { Assert.Equal("public", dbModel.DefaultSchema); @@ -145,8 +145,8 @@ public void Create_tables() CREATE TABLE "Everest" (id int); CREATE TABLE "Denali" (id int); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { Assert.Collection( @@ -178,8 +178,8 @@ public void Filter_schemas() CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - Enumerable.Empty(), - new[] { "db2" }, + [], + ["db2"], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -201,8 +201,8 @@ public void Filter_tables() CREATE TABLE "K2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B), FOREIGN KEY (B) REFERENCES "K2" (A)); """, - new[] { "K2" }, - Enumerable.Empty(), + ["K2"], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -224,8 +224,10 @@ public void Filter_tables_with_qualified_name() CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - new[] { @"""K.2""" }, - Enumerable.Empty(), + [""" + "K.2" + """], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -248,8 +250,8 @@ public void Filter_tables_with_schema_qualified_name1() CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - new[] { "public.K2" }, - Enumerable.Empty(), + ["public.K2"], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -273,8 +275,10 @@ public void Filter_tables_with_schema_qualified_name2() CREATE TABLE "db.2"."K.2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - new[] { @"""db.2"".""K.2""" }, - Enumerable.Empty(), + [""" + "db.2"."K.2" + """], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -298,8 +302,10 @@ public void Filter_tables_with_schema_qualified_name3() CREATE TABLE "db2"."K.2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - new[] { @"public.""K.2""" }, - Enumerable.Empty(), + [""" + public."K.2" + """], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -323,8 +329,10 @@ public void Filter_tables_with_schema_qualified_name4() CREATE TABLE "db.2"."K2" (Id int, A varchar, UNIQUE (A)); CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); """, - new[] { @"""db.2"".K2" }, - Enumerable.Empty(), + [""" + "db.2".K2 + """], + [], dbModel => { var table = Assert.Single(dbModel.Tables); @@ -380,15 +388,24 @@ public void Complex_filtering_validation() FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "db2"."PrincipalTable"("UC1", "UC2") ON DELETE CASCADE ); """, - new[] - { - @"""db.2"".""QuotedTableName""", - @"""db.2"".SimpleTableName", - @"public.""Table.With.Dot""", - @"public.""SimpleTableName""", - @"""JustTableName""" - }, - new[] { "db2" }, + [ + """ + "db.2"."QuotedTableName" + """, + """ + "db.2".SimpleTableName + """, + """ + public."Table.With.Dot" + """, + """ + public."SimpleTableName" + """, + """ + "JustTableName" + """ + ], + ["db2"], dbModel => { var sequence = Assert.Single(dbModel.Sequences); @@ -396,11 +413,11 @@ FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "db2"."PrincipalTable" Assert.Equal("db2", sequence.Schema); Assert.Single(dbModel.Tables.Where(t => t.Schema == "db.2" && t.Name == "QuotedTableName")); - Assert.Empty(dbModel.Tables.Where(t => t.Schema == "db.2" && t.Name == "Table.With.Dot")); + Assert.DoesNotContain(dbModel.Tables, t => t.Schema == "db.2" && t.Name == "Table.With.Dot"); Assert.Single(dbModel.Tables.Where(t => t.Schema == "db.2" && t.Name == "SimpleTableName")); Assert.Single(dbModel.Tables.Where(t => t.Schema == "db.2" && t.Name == "JustTableName")); - Assert.Empty(dbModel.Tables.Where(t => t.Schema == "public" && t.Name == "QuotedTableName")); + Assert.DoesNotContain(dbModel.Tables, t => t.Schema == "public" && t.Name == "QuotedTableName"); Assert.Single(dbModel.Tables.Where(t => t.Schema == "public" && t.Name == "Table.With.Dot")); Assert.Single(dbModel.Tables.Where(t => t.Schema == "public" && t.Name == "SimpleTableName")); Assert.Single(dbModel.Tables.Where(t => t.Schema == "public" && t.Name == "JustTableName")); @@ -455,8 +472,8 @@ public void Create_columns() "Name" text NOT NULL ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -472,7 +489,9 @@ public void Create_columns() Assert.Single(table.Columns.Where(c => c.Name == "Id")); Assert.Single(table.Columns.Where(c => c.Name == "Name")); }, - @"DROP TABLE ""Blogs"""); + """ + DROP TABLE "Blogs" + """); [Fact] public void Create_view_columns() @@ -480,8 +499,8 @@ public void Create_view_columns() """ CREATE VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = Assert.IsType(dbModel.Tables.Single()); @@ -498,7 +517,7 @@ public void Create_view_columns() Assert.Single(table.Columns.Where(c => c.Name == "Id")); Assert.Single(table.Columns.Where(c => c.Name == "Name")); }, - @"DROP VIEW ""BlogsView"";"); + """DROP VIEW "BlogsView";"""); [Fact] public void Create_materialized_view_columns() @@ -506,8 +525,8 @@ public void Create_materialized_view_columns() """ CREATE MATERIALIZED VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -524,7 +543,7 @@ public void Create_materialized_view_columns() Assert.Single(table.Columns.Where(c => c.Name == "Id")); Assert.Single(table.Columns.Where(c => c.Name == "Name")); }, - @"DROP MATERIALIZED VIEW ""BlogsView"";"); + """DROP MATERIALIZED VIEW "BlogsView";"""); [Fact] public void Create_primary_key() @@ -532,8 +551,8 @@ public void Create_primary_key() """ CREATE TABLE "PrimaryKeyTable" ("Id" int PRIMARY KEY); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var pk = dbModel.Tables.Single().PrimaryKey; @@ -541,9 +560,11 @@ public void Create_primary_key() Assert.Equal("public", pk.Table.Schema); Assert.Equal("PrimaryKeyTable", pk.Table.Name); Assert.StartsWith("PrimaryKeyTable_pkey", pk.Name); - Assert.Equal(new List { "Id" }, pk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], pk.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""PrimaryKeyTable"""); + """ + DROP TABLE "PrimaryKeyTable" + """); [Fact] public void Create_unique_constraints() @@ -560,8 +581,8 @@ public void Create_unique_constraints() CREATE INDEX "IX_INDEX" on "UniqueConstraint" ("IndexProperty"); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -571,14 +592,16 @@ public void Create_unique_constraints() Assert.Equal("public", firstConstraint.Table.Schema); Assert.Equal("UniqueConstraint", firstConstraint.Table.Name); //Assert.StartsWith("UQ__UniqueCo", uniqueConstraint.Name); - Assert.Equal(new List { "Name" }, firstConstraint.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Name"], firstConstraint.Columns.Select(ic => ic.Name).ToList()); var secondConstraint = table.UniqueConstraints.Single(c => c.Columns.Count == 2); Assert.Equal("public", secondConstraint.Table.Schema); Assert.Equal("UniqueConstraint", secondConstraint.Table.Name); - Assert.Equal(new List { "Unq1", "Unq2" }, secondConstraint.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Unq1", "Unq2"], secondConstraint.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""UniqueConstraint"""); + """ + DROP TABLE "UniqueConstraint" + """); [Fact] public void Create_indexes() @@ -595,8 +618,8 @@ public void Create_indexes() CREATE INDEX "IX_NAME" on "IndexTable" ("Name"); CREATE INDEX "IX_INDEX" on "IndexTable" ("IndexProperty"); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -613,7 +636,9 @@ public void Create_indexes() Assert.Single(table.Indexes.Where(c => c.Name == "IX_NAME")); Assert.Single(table.Indexes.Where(c => c.Name == "IX_INDEX")); }, - @"DROP TABLE ""IndexTable"""); + """ + DROP TABLE "IndexTable" + """); [Fact] public void Create_foreign_keys() @@ -634,8 +659,8 @@ FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE FOREIGN KEY ("Id") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var firstFk = Assert.Single(dbModel.Tables.Single(t => t.Name == "FirstDependent").ForeignKeys); @@ -645,8 +670,8 @@ FOREIGN KEY ("Id") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION Assert.Equal("FirstDependent", firstFk.Table.Name); Assert.Equal("public", firstFk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", firstFk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId" }, firstFk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id" }, firstFk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId"], firstFk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], firstFk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, firstFk.OnDelete); var secondFk = Assert.Single(dbModel.Tables.Single(t => t.Name == "SecondDependent").ForeignKeys); @@ -656,8 +681,8 @@ FOREIGN KEY ("Id") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION Assert.Equal("SecondDependent", secondFk.Table.Name); Assert.Equal("public", secondFk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", secondFk.PrincipalTable.Name); - Assert.Equal(new List { "Id" }, secondFk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id" }, secondFk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], secondFk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], secondFk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.NoAction, secondFk.OnDelete); }, """ @@ -688,8 +713,8 @@ CREATE TABLE domains ( char_domain public.char_domain NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var textDomainColumn = Assert.Single(dbModel.Tables.Single().Columns.Where(c => c.Name == "text_domain")); @@ -721,8 +746,8 @@ public void Decimal_numeric_types_have_precision_scale() "numeric18Column" numeric(18) NOT NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -731,7 +756,9 @@ public void Decimal_numeric_types_have_precision_scale() Assert.Equal("numeric(15,2)", columns.Single(c => c.Name == "numeric152Column").StoreType); Assert.Equal("numeric(18,0)", columns.Single(c => c.Name == "numeric18Column").StoreType); }, - @"DROP TABLE ""NumericColumns"""); + """ + DROP TABLE "NumericColumns" + """); [Fact] public void Specific_max_length_are_add_to_store_type() @@ -747,8 +774,8 @@ public void Specific_max_length_are_add_to_store_type() "varbit123ArrayColumn" varbit(123)[] NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -760,7 +787,9 @@ public void Specific_max_length_are_add_to_store_type() Assert.Equal("character varying(66)[]", columns.Single(c => c.Name == "varchar66ArrayColumn").StoreType); Assert.Equal("bit varying(123)[]", columns.Single(c => c.Name == "varbit123ArrayColumn").StoreType); }, - @"DROP TABLE ""LengthColumns"""); + """ + DROP TABLE "LengthColumns" + """); [Fact] public void Datetime_types_have_precision_if_non_null_scale() @@ -775,8 +804,8 @@ public void Datetime_types_have_precision_if_non_null_scale() "interval5Column" interval(5) NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -787,7 +816,9 @@ public void Datetime_types_have_precision_if_non_null_scale() Assert.Equal("timestamp(4) with time zone", columns.Single(c => c.Name == "timestamptz4Column").StoreType); Assert.Equal("interval(5)", columns.Single(c => c.Name == "interval5Column").StoreType); }, - @"DROP TABLE ""LengthColumns"""); + """ + DROP TABLE "LengthColumns" + """); [Fact] public void Store_types_without_any_facets() @@ -820,8 +851,8 @@ public void Store_types_without_any_facets() "textArrayColumn" text[] ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single(t => t.Name == "NoFacetTypes").Columns; @@ -849,7 +880,9 @@ public void Store_types_without_any_facets() Assert.Equal("xid", columns.Single(c => c.Name == "xidColumn").StoreType); Assert.Equal("text[]", columns.Single(c => c.Name == "textArrayColumn").StoreType); }, - @"DROP TABLE ""NoFacetTypes"""); + """ + DROP TABLE "NoFacetTypes" + """); [Fact] public void Default_values_are_stored() @@ -860,8 +893,8 @@ public void Default_values_are_stored() "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -869,7 +902,9 @@ public void Default_values_are_stored() "'1999-01-08 00:00:00'::timestamp without time zone", columns.Single(c => c.Name == "FixedDefaultValue").DefaultValueSql); }, - @"DROP TABLE ""DefaultValues"""); + """ + DROP TABLE "DefaultValues" + """); [ConditionalFact] [MinimumPostgresVersion(12, 0)] @@ -883,8 +918,8 @@ public void Computed_values_are_stored() "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -893,10 +928,12 @@ public void Computed_values_are_stored() // columns. var column = columns.Single(c => c.Name == "SumOfAAndB"); Assert.Null(column.DefaultValueSql); - Assert.Equal(@"(""A"" + ""B"")", column.ComputedColumnSql); + Assert.Equal("""("A" + "B")""", column.ComputedColumnSql); Assert.True(column.IsStored); }, - @"DROP TABLE ""ComputedValues"""); + """ + DROP TABLE "ComputedValues" + """); [Fact] public void Default_value_matching_clr_default_is_not_stored() @@ -927,8 +964,8 @@ public void Default_value_matching_clr_default_is_not_stored() "IgnoredDefault34" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000' ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -952,8 +989,8 @@ public void ValueGenerated_is_set_for_default_and_serial_column() "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -962,7 +999,9 @@ public void ValueGenerated_is_set_for_default_and_serial_column() Assert.Null(columns.Single(c => c.Name == "NoValueGenerationColumn").ValueGenerated); Assert.Null(columns.Single(c => c.Name == "FixedDefaultValue").ValueGenerated); }, - @"DROP TABLE ""ValueGeneratedProperties"""); + """ + DROP TABLE "ValueGeneratedProperties" + """); [ConditionalFact] [MinimumPostgresVersion(10, 0)] @@ -974,8 +1013,8 @@ public void ValueGenerated_is_set_for_identity_column() "Id2" INT GENERATED BY DEFAULT AS IDENTITY ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -983,7 +1022,9 @@ public void ValueGenerated_is_set_for_identity_column() Assert.Equal(ValueGenerated.OnAdd, columns.Single(c => c.Name == "Id1").ValueGenerated); Assert.Equal(ValueGenerated.OnAdd, columns.Single(c => c.Name == "Id2").ValueGenerated); }, - @"DROP TABLE ""ValueGeneratedProperties"""); + """ + DROP TABLE "ValueGeneratedProperties" + """); [ConditionalFact] [MinimumPostgresVersion(12, 0)] @@ -997,15 +1038,17 @@ public void ValueGenerated_is_set_for_computed_column() "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; Assert.Null(columns.Single(c => c.Name == "SumOfAAndB").ValueGenerated); }, - @"DROP TABLE ""ValueGeneratedProperties"""); + """ + DROP TABLE "ValueGeneratedProperties" + """); [Fact] public void Column_nullability_is_set() @@ -1017,8 +1060,8 @@ public void Column_nullability_is_set() "NonNullableInt" int NOT NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -1026,7 +1069,9 @@ public void Column_nullability_is_set() Assert.True(columns.Single(c => c.Name == "NullableInt").IsNullable); Assert.False(columns.Single(c => c.Name == "NonNullableInt").IsNullable); }, - @"DROP TABLE ""NullableColumns"""); + """ + DROP TABLE "NullableColumns" + """); [Fact] public void Column_nullability_is_set_with_domain() @@ -1040,8 +1085,8 @@ public void Column_nullability_is_set_with_domain() "NonNullString" non_nullable_int NOT NULL ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -1063,10 +1108,12 @@ CREATE TABLE "SystemColumnsTable" "Id" int NOT NULL PRIMARY KEY ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => Assert.Single(dbModel.Tables.Single().Columns), - @"DROP TABLE ""SystemColumnsTable"""); + """ + DROP TABLE "SystemColumnsTable" + """); #endregion @@ -1082,17 +1129,19 @@ public void Create_composite_primary_key() PRIMARY KEY ("Id2", "Id1") ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var pk = dbModel.Tables.Single().PrimaryKey; Assert.Equal("public", pk.Table.Schema); Assert.Equal("CompositePrimaryKeyTable", pk.Table.Name); - Assert.Equal(new List { "Id2", "Id1" }, pk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2", "Id1"], pk.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""CompositePrimaryKeyTable"""); + """ + DROP TABLE "CompositePrimaryKeyTable" + """); [Fact] public void Set_primary_key_name_from_index() @@ -1104,8 +1153,8 @@ public void Set_primary_key_name_from_index() CONSTRAINT "MyPK" PRIMARY KEY ( "Id2" ) ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var pk = dbModel.Tables.Single().PrimaryKey; @@ -1113,9 +1162,11 @@ public void Set_primary_key_name_from_index() Assert.Equal("public", pk.Table.Schema); Assert.Equal("PrimaryKeyName", pk.Table.Name); Assert.StartsWith("MyPK", pk.Name); - Assert.Equal(new List { "Id2" }, pk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2"], pk.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""PrimaryKeyName"""); + """ + DROP TABLE "PrimaryKeyName" + """); #endregion @@ -1131,8 +1182,8 @@ public void Create_composite_unique_constraint() CONSTRAINT "UX" UNIQUE ("Id2", "Id1") ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var uniqueConstraint = Assert.Single(dbModel.Tables.Single().UniqueConstraints); @@ -1141,9 +1192,11 @@ public void Create_composite_unique_constraint() Assert.Equal("public", uniqueConstraint.Table.Schema); Assert.Equal("CompositeUniqueConstraintTable", uniqueConstraint.Table.Name); Assert.Equal("UX", uniqueConstraint.Name); - Assert.Equal(new List { "Id2", "Id1" }, uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2", "Id1"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""CompositeUniqueConstraintTable"""); + """ + DROP TABLE "CompositeUniqueConstraintTable" + """); [Fact] public void Set_unique_constraint_name_from_index() @@ -1155,8 +1208,8 @@ public void Set_unique_constraint_name_from_index() CONSTRAINT "MyUC" UNIQUE ( "Id2" ) ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1166,10 +1219,12 @@ public void Set_unique_constraint_name_from_index() Assert.Equal("public", uniqueConstraint.Table.Schema); Assert.Equal("UniqueConstraintName", uniqueConstraint.Table.Name); Assert.Equal("MyUC", uniqueConstraint.Name); - Assert.Equal(new List { "Id2" }, uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); Assert.Empty(table.Indexes); }, - @"DROP TABLE ""UniqueConstraintName"""); + """ + DROP TABLE "UniqueConstraintName" + """); #endregion @@ -1186,8 +1241,8 @@ public void Create_composite_index() CREATE INDEX "IX_COMPOSITE" ON "CompositeIndexTable" ( "Id2", "Id1" ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var index = Assert.Single(dbModel.Tables.Single().Indexes); @@ -1196,9 +1251,11 @@ public void Create_composite_index() Assert.Equal("public", index.Table.Schema); Assert.Equal("CompositeIndexTable", index.Table.Name); Assert.Equal("IX_COMPOSITE", index.Name); - Assert.Equal(new List { "Id2", "Id1" }, index.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2", "Id1"], index.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""CompositeIndexTable"""); + """ + DROP TABLE "CompositeIndexTable" + """); [Fact] public void Set_unique_true_for_unique_index() @@ -1211,8 +1268,8 @@ public void Set_unique_true_for_unique_index() CREATE UNIQUE INDEX "IX_UNIQUE" ON "UniqueIndexTable" ( "Id2" ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var index = Assert.Single(dbModel.Tables.Single().Indexes); @@ -1223,9 +1280,11 @@ public void Set_unique_true_for_unique_index() Assert.Equal("IX_UNIQUE", index.Name); Assert.True(index.IsUnique); Assert.Null(index.Filter); - Assert.Equal(new List { "Id2" }, index.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""UniqueIndexTable"""); + """ + DROP TABLE "UniqueIndexTable" + """); [Fact] public void Set_filter_for_filtered_index() @@ -1238,8 +1297,8 @@ public void Set_filter_for_filtered_index() CREATE UNIQUE INDEX "IX_UNIQUE" ON "FilteredIndexTable" ( "Id2" ) WHERE "Id2" > 10; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var index = Assert.Single(dbModel.Tables.Single().Indexes); @@ -1248,10 +1307,12 @@ public void Set_filter_for_filtered_index() Assert.Equal("public", index.Table.Schema); Assert.Equal("FilteredIndexTable", index.Table.Name); Assert.Equal("IX_UNIQUE", index.Name); - Assert.Equal(@"(""Id2"" > 10)", index.Filter); - Assert.Equal(new List { "Id2" }, index.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal("""("Id2" > 10)""", index.Filter); + Assert.Equal(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); }, - @"DROP TABLE ""FilteredIndexTable"""); + """ + DROP TABLE "FilteredIndexTable" + """); #endregion @@ -1274,8 +1335,8 @@ PRIMARY KEY ("Id1", "Id2") FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "PrincipalTable"("Id1", "Id2") ON DELETE CASCADE ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); @@ -1285,8 +1346,8 @@ FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "PrincipalTable"("Id1" Assert.Equal("DependentTable", fk.Table.Name); Assert.Equal("public", fk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId1", "ForeignKeyId2" }, fk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id1", "Id2" }, fk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId1", "ForeignKeyId2"], fk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id1", "Id2"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, fk.OnDelete); }, """ @@ -1314,8 +1375,8 @@ FOREIGN KEY ("ForeignKeyId1") REFERENCES "PrincipalTable"("Id") ON DELETE CASCAD FOREIGN KEY ("ForeignKeyId2") REFERENCES "AnotherPrincipalTable"("Id") ON DELETE CASCADE ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var foreignKeys = dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys; @@ -1329,8 +1390,8 @@ FOREIGN KEY ("ForeignKeyId2") REFERENCES "AnotherPrincipalTable"("Id") ON DELETE Assert.Equal("DependentTable", principalFk.Table.Name); Assert.Equal("public", principalFk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", principalFk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId1" }, principalFk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id" }, principalFk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId1"], principalFk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], principalFk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, principalFk.OnDelete); var anotherPrincipalFk = Assert.Single(foreignKeys.Where(f => f.PrincipalTable.Name == "AnotherPrincipalTable")); @@ -1340,8 +1401,8 @@ FOREIGN KEY ("ForeignKeyId2") REFERENCES "AnotherPrincipalTable"("Id") ON DELETE Assert.Equal("DependentTable", anotherPrincipalFk.Table.Name); Assert.Equal("public", anotherPrincipalFk.PrincipalTable.Schema); Assert.Equal("AnotherPrincipalTable", anotherPrincipalFk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId2" }, anotherPrincipalFk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id" }, anotherPrincipalFk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId2"], anotherPrincipalFk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], anotherPrincipalFk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, anotherPrincipalFk.OnDelete); }, """ @@ -1365,8 +1426,8 @@ public void Create_foreign_key_referencing_unique_constraint() FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id2") ON DELETE CASCADE ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); @@ -1376,8 +1437,8 @@ FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id2") ON DELETE CASCAD Assert.Equal("DependentTable", fk.Table.Name); Assert.Equal("public", fk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId" }, fk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id2" }, fk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId"], fk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id2"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, fk.OnDelete); }, """ @@ -1399,8 +1460,8 @@ public void Set_name_for_foreign_key() CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); @@ -1410,8 +1471,8 @@ public void Set_name_for_foreign_key() Assert.Equal("DependentTable", fk.Table.Name); Assert.Equal("public", fk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - Assert.Equal(new List { "ForeignKeyId" }, fk.Columns.Select(ic => ic.Name).ToList()); - Assert.Equal(new List { "Id" }, fk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["ForeignKeyId"], fk.Columns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); Assert.Equal(ReferentialAction.Cascade, fk.OnDelete); // ReSharper disable once StringLiteralTypo Assert.Equal("MYFK", fk.Name); @@ -1443,8 +1504,8 @@ FOREIGN KEY ("ForeignKeyRestrictId") REFERENCES "PrincipalTable"("Id") ON DELETE FOREIGN KEY ("ForeignKeySetDefaultId") REFERENCES "PrincipalTable"("Id") ON DELETE SET DEFAULT ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(t => t.Name == "DependentTable"); @@ -1455,7 +1516,7 @@ FOREIGN KEY ("ForeignKeySetDefaultId") REFERENCES "PrincipalTable"("Id") ON DELE Assert.Equal("DependentTable", fk.Table.Name); Assert.Equal("public", fk.PrincipalTable.Schema); Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - Assert.Equal(new List { "Id" }, fk.PrincipalColumns.Select(ic => ic.Name).ToList()); + Assert.Equal(["Id"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); } Assert.Equal( @@ -1487,8 +1548,8 @@ public void Warn_missing_schema() """ CREATE TABLE "Blank" ("Id" int) """, - Enumerable.Empty(), - new[] { "MySchema" }, + [], + ["MySchema"], dbModel => { Assert.Empty(dbModel.Tables); @@ -1499,7 +1560,9 @@ public void Warn_missing_schema() Assert.Equal( NpgsqlResources.LogMissingSchema(new TestLogger()).GenerateMessage("MySchema"), Message); }, - @"DROP TABLE ""Blank"""); + """ + DROP TABLE "Blank" + """); [Fact] public void Warn_missing_table() @@ -1507,8 +1570,8 @@ public void Warn_missing_table() """ CREATE TABLE "Blank" ("Id" int) """, - new[] { "MyTable" }, - Enumerable.Empty(), + ["MyTable"], + [], dbModel => { Assert.Empty(dbModel.Tables); @@ -1519,7 +1582,9 @@ public void Warn_missing_table() Assert.Equal( NpgsqlResources.LogMissingTable(new TestLogger()).GenerateMessage("MyTable"), Message); }, - @"DROP TABLE ""Blank"""); + """ + DROP TABLE "Blank" + """); [Fact] public void Warn_missing_principal_table_for_foreign_key() @@ -1535,8 +1600,8 @@ public void Warn_missing_principal_table_for_foreign_key() CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE ); """, - new[] { "DependentTable" }, - Enumerable.Empty(), + ["DependentTable"], + [], _ => { var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log.Where(t => t.Level == LogLevel.Warning)); @@ -1565,8 +1630,8 @@ public void SequenceSerial() CREATE TABLE my_schema.serial_sequence_in_schema (Id serial PRIMARY KEY); CREATE TABLE my_schema."SerialSequenceInSchema" ("Id" serial PRIMARY KEY); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { // Sequences which belong to a serial column should not get reverse engineered as separate sequences @@ -1595,12 +1660,12 @@ public void SequenceNonSerial() CREATE SEQUENCE "SomeSequence"; CREATE TABLE "NonSerialSequence" ("Id" integer PRIMARY KEY DEFAULT nextval('"SomeSequence"')) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var column = dbModel.Tables.Single().Columns.Single(); - Assert.Equal(@"nextval('""SomeSequence""'::regclass)", column.DefaultValueSql); + Assert.Equal("""nextval('"SomeSequence"'::regclass)""", column.DefaultValueSql); // Npgsql has special detection for serial columns (scaffolding them with ValueGenerated.OnAdd // and removing the default), but not for non-serial sequence-driven columns, which are scaffolded // with a DefaultValue. This is consistent with the SqlServer scaffolding behavior. @@ -1624,8 +1689,8 @@ CREATE TABLE identity ( b int GENERATED BY DEFAULT AS IDENTITY ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var idIdentityAlways = dbModel.Tables.Single().Columns.Single(c => c.Name == "id"); @@ -1663,8 +1728,8 @@ with_options int GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 M smallint_without_options smallint GENERATED BY DEFAULT AS IDENTITY ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var withOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "with_options"); @@ -1715,8 +1780,8 @@ CREATE TABLE columns_with_collation ( non_default_collation TEXT COLLATE "POSIX" ); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var columns = dbModel.Tables.Single().Columns; @@ -1730,8 +1795,8 @@ non_default_collation TEXT COLLATE "POSIX" public void Default_database_collation_is_not_scaffolded() => Test( @"-- Empty database", - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => Assert.Null(dbModel.Collation), @""); @@ -1743,8 +1808,8 @@ public void Index_method() CREATE INDEX ix_a ON "IndexMethod" USING hash (a); CREATE INDEX ix_b ON "IndexMethod" (b); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1762,7 +1827,9 @@ public void Index_method() Assert.Null(noMethodIndex.FindAnnotation(NpgsqlAnnotationNames.IndexMethod)); //Assert.Equal("btree", noMethodIndex.FindAnnotation(NpgsqlAnnotationNames.IndexMethod).Value); }, - @"DROP TABLE ""IndexMethod"""); + """ + DROP TABLE "IndexMethod" + """); [Fact] public void Index_operators() @@ -1772,8 +1839,8 @@ public void Index_operators() CREATE INDEX ix_with ON "IndexOperators" (a, b varchar_pattern_ops); CREATE INDEX ix_without ON "IndexOperators" (a, b); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1784,7 +1851,9 @@ public void Index_operators() var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexOperators)); }, - @"DROP TABLE ""IndexOperators"""); + """ + DROP TABLE "IndexOperators" + """); [Fact] public void Index_collation() @@ -1794,8 +1863,8 @@ public void Index_collation() CREATE INDEX ix_with ON "IndexCollation" (a, b COLLATE "POSIX"); CREATE INDEX ix_without ON "IndexCollation" (a, b); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1806,7 +1875,9 @@ public void Index_collation() var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); Assert.Null(indexWithout.FindAnnotation(RelationalAnnotationNames.Collation)); }, - @"DROP TABLE ""IndexCollation"""); + """ + DROP TABLE "IndexCollation" + """); [Theory] [InlineData("gin", new bool[0])] @@ -1825,8 +1896,8 @@ public void Index_IsDescending(string method, bool[] expected) CREATE INDEX ix_btree ON "IndexSortOrder" USING btree (a ASC, b DESC); CREATE INDEX ix_without ON "IndexSortOrder" (a, b); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1836,9 +1907,11 @@ public void Index_IsDescending(string method, bool[] expected) Assert.Equal(expected, indexWith.IsDescending); var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Equal(new[] { false, false }, indexWithout.IsDescending); + Assert.Equal([false, false], indexWithout.IsDescending); }, - @"DROP TABLE ""IndexSortOrder"""); + """ + DROP TABLE "IndexSortOrder" + """); [Fact] public void Index_null_sort_order() @@ -1848,8 +1921,8 @@ public void Index_null_sort_order() CREATE INDEX ix_with ON "IndexNullSortOrder" (a NULLS FIRST, b DESC NULLS LAST); CREATE INDEX ix_without ON "IndexNullSortOrder" (a, b); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1862,7 +1935,9 @@ public void Index_null_sort_order() var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder)); }, - @"DROP TABLE ""IndexNullSortOrder"""); + """ + DROP TABLE "IndexNullSortOrder" + """); [ConditionalFact] [MinimumPostgresVersion(11, 0)] @@ -1873,8 +1948,8 @@ public void Index_covering() CREATE INDEX ix_with ON "IndexCovering" (a) INCLUDE (b, c); CREATE INDEX ix_without ON "IndexCovering" (a, b, c); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1889,7 +1964,9 @@ public void Index_covering() Assert.Equal(new[] { "a", "b", "c" }, indexWithout.Columns.Select(i => i.Name).ToArray()); Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexInclude)); }, - @"DROP TABLE ""IndexCovering"""); + """ + DROP TABLE "IndexCovering" + """); [ConditionalFact] [MinimumPostgresVersion(15, 0)] @@ -1900,8 +1977,8 @@ public void Index_are_nulls_distinct() CREATE INDEX "IX_NullsDistinct" ON "IndexNullsDistinct" (a); CREATE INDEX "IX_NullsNotDistinct" ON "IndexNullsDistinct" (a) NULLS NOT DISTINCT; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1913,7 +1990,9 @@ public void Index_are_nulls_distinct() false, Assert.Single(table.Indexes, i => i.Name == "IX_NullsNotDistinct")[NpgsqlAnnotationNames.NullsDistinct]); }, - @"DROP TABLE ""IndexNullsDistinct"""); + """ + DROP TABLE "IndexNullsDistinct" + """); [Fact] public void Comments() @@ -1923,8 +2002,8 @@ public void Comments() COMMENT ON TABLE comment IS 'table comment'; COMMENT ON COLUMN comment.a IS 'column comment' """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var table = dbModel.Tables.Single(); @@ -1942,8 +2021,8 @@ public void Sequence_types() CREATE SEQUENCE "IntSequence" AS int; CREATE SEQUENCE "BigIntSequence" AS bigint; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var smallSequence = dbModel.Sequences.Single(s => s.Name == "SmallIntSequence"); @@ -1967,8 +2046,8 @@ public void Dropped_columns() ALTER TABLE foo DROP COLUMN id; ALTER TABLE foo ADD COLUMN id2 int PRIMARY KEY; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { Assert.Single(dbModel.Tables.Single().Columns); @@ -1979,11 +2058,12 @@ public void Dropped_columns() public void Postgres_extensions() => Test( """ +DROP EXTENSION IF EXISTS postgis; CREATE EXTENSION hstore; CREATE EXTENSION pgcrypto SCHEMA db2; """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var extensions = dbModel.GetPostgresExtensions(); @@ -2010,8 +2090,8 @@ public void Enums() CREATE TYPE db2.mood AS ENUM ('excited', 'depressed'); CREATE TABLE foo (mood mood UNIQUE); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var enums = dbModel.GetPostgresEnums(); @@ -2019,11 +2099,11 @@ public void Enums() var mood = enums.Single(e => e.Schema is null); Assert.Equal("mood", mood.Name); - Assert.Equal(new[] { "happy", "sad" }, mood.Labels); + Assert.Equal(["happy", "sad"], mood.Labels); var mood2 = enums.Single(e => e.Schema == "db2"); Assert.Equal("mood", mood2.Name); - Assert.Equal(new[] { "excited", "depressed" }, mood2.Labels); + Assert.Equal(["excited", "depressed"], mood2.Labels); var table = Assert.Single(dbModel.Tables); Assert.NotNull(table); @@ -2048,8 +2128,8 @@ public void Bug453() CREATE TABLE foo (mood mood, some_num int UNIQUE); CREATE TABLE bar (foreign_key int REFERENCES foo(some_num)); """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], // Enum columns are left out of the model for now (a warning is logged). dbModel => Assert.Single(dbModel.Tables.Single(t => t.Name == "foo").Columns), """ @@ -2092,8 +2172,8 @@ CREATE TABLE column_types ( line line ) """, - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], dbModel => { var options = new NpgsqlSingletonOptions(); @@ -2103,9 +2183,9 @@ line line new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty() + [] ), - new RelationalTypeMappingSourceDependencies(Array.Empty()), + new RelationalTypeMappingSourceDependencies([]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), options); @@ -2124,13 +2204,13 @@ line line [RequiresPostgis] public void System_tables_are_ignored() => Test( - "CREATE EXTENSION postgis", - Enumerable.Empty(), - Enumerable.Empty(), - dbModel => - { - Assert.Empty(dbModel.Tables); - }, + """ +DROP EXTENSION IF EXISTS postgis; +CREATE EXTENSION postgis; +""", + [], + [], + dbModel => Assert.Empty(dbModel.Tables), "DROP EXTENSION postgis"); #endregion @@ -2183,7 +2263,9 @@ public override async Task InitializeAsync() { await base.InitializeAsync(); await TestStore.ExecuteNonQueryAsync("CREATE SCHEMA IF NOT EXISTS db2"); - await TestStore.ExecuteNonQueryAsync(@"CREATE SCHEMA IF NOT EXISTS ""db.2"""); + await TestStore.ExecuteNonQueryAsync(""" + CREATE SCHEMA IF NOT EXISTS "db.2" + """); } protected override bool ShouldLogCategory(string logCategory) diff --git a/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs index 8844da5a8..90df831ea 100644 --- a/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs @@ -10,13 +10,8 @@ protected override TestStore TestStore protected override SeedingContext CreateContextWithEmptyDatabase(string testId) => new SeedingNpgsqlContext(testId); - protected class SeedingNpgsqlContext : SeedingContext + protected class SeedingNpgsqlContext(string testId) : SeedingContext(testId) { - public SeedingNpgsqlContext(string testId) - : base(testId) - { - } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(NpgsqlTestStore.CreateConnectionString($"Seeds{TestId}")); } diff --git a/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs b/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs index 51d9e76a3..715fa78fa 100644 --- a/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs +++ b/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs @@ -2,7 +2,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class SequenceEndToEndTest : IDisposable +public class SequenceEndToEndTest : IAsyncLifetime { [ConditionalFact] public void Can_use_sequence_end_to_end() @@ -263,23 +263,14 @@ private static void AddEntitiesWithIds(IServiceProvider serviceProvider, int idO context.SaveChanges(); } - private class BronieContext : DbContext + private class BronieContext(IServiceProvider serviceProvider, string databaseName) : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly string _databaseName; - - public BronieContext(IServiceProvider serviceProvider, string databaseName) - { - _serviceProvider = serviceProvider; - _databaseName = databaseName; - } - public DbSet Pegasuses { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseInternalServiceProvider(_serviceProvider) - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()); + .UseInternalServiceProvider(serviceProvider) + .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( @@ -366,32 +357,22 @@ private static void AddEntitiesNullable(IServiceProvider serviceProvider, string context.SaveChanges(); } - private class NullableBronieContext : DbContext + private class NullableBronieContext(IServiceProvider serviceProvider, string databaseName, bool useSequence) + : DbContext { - private readonly IServiceProvider _serviceProvider; - private readonly string _databaseName; - private readonly bool _useSequence; - - public NullableBronieContext(IServiceProvider serviceProvider, string databaseName, bool useSequence) - { - _serviceProvider = serviceProvider; - _databaseName = databaseName; - _useSequence = useSequence; - } - public DbSet Unicons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseInternalServiceProvider(_serviceProvider) - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()); + .UseInternalServiceProvider(serviceProvider) + .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( b => { b.HasKey(e => e.Identifier); - if (_useSequence) + if (useSequence) { b.Property(e => e.Identifier).UseHiLo(); } @@ -408,13 +389,11 @@ private class Unicon public string Name { get; set; } } - public SequenceEndToEndTest() - { - TestStore = NpgsqlTestStore.CreateInitialized("SequenceEndToEndTest"); - } + protected NpgsqlTestStore TestStore { get; private set; } - protected NpgsqlTestStore TestStore { get; } + public async Task InitializeAsync() + => TestStore = await NpgsqlTestStore.CreateInitializedAsync("SequenceEndToEndTest"); - public void Dispose() - => TestStore.Dispose(); + public async Task DisposeAsync() + => await TestStore.DisposeAsync(); } diff --git a/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs index 8fb43e9e4..c0f16c72e 100644 --- a/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs @@ -1,9 +1,3 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class SerializationNpgsqlTest : SerializationTestBase -{ - public SerializationNpgsqlTest(F1BytesNpgsqlFixture fixture) - : base(fixture) - { - } -} +public class SerializationNpgsqlTest(F1BytesNpgsqlFixture fixture) : SerializationTestBase(fixture); 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 17772cfb0..dc949e01d 100644 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs @@ -3,13 +3,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; [RequiresPostgis] -public class SpatialNpgsqlTest : SpatialTestBase +public class SpatialNpgsqlTest(SpatialNpgsqlFixture fixture) : SpatialTestBase(fixture) { - public SpatialNpgsqlTest(SpatialNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => 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 Task Mutation_of_tracked_values_does_not_mutate_values_in_store() + => Assert.ThrowsAsync(() => base.Mutation_of_tracked_values_does_not_mutate_values_in_store()); } diff --git a/test/EFCore.PG.FunctionalTests/StoreGeneratedFixupNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/StoreGeneratedFixupNpgsqlTest.cs index 1b967b6e2..c290140b1 100644 --- a/test/EFCore.PG.FunctionalTests/StoreGeneratedFixupNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/StoreGeneratedFixupNpgsqlTest.cs @@ -2,18 +2,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class StoreGeneratedFixupNpgsqlTest : StoreGeneratedFixupRelationalTestBase< - StoreGeneratedFixupNpgsqlTest.StoreGeneratedFixupNpgsqlFixture> +public class StoreGeneratedFixupNpgsqlTest(StoreGeneratedFixupNpgsqlTest.StoreGeneratedFixupNpgsqlFixture fixture) + : StoreGeneratedFixupRelationalTestBase(fixture) { - public StoreGeneratedFixupNpgsqlTest(StoreGeneratedFixupNpgsqlFixture fixture) - : base(fixture) - { - } - [Fact] - public void Temp_values_are_replaced_on_save() - => ExecuteWithStrategyInTransaction( - context => + public Task Temp_values_are_replaced_on_save() + => ExecuteWithStrategyInTransactionAsync( + async context => { var entry = context.Add(new TestTemp()); @@ -22,7 +17,7 @@ public void Temp_values_are_replaced_on_save() var tempValue = entry.Property(e => e.Id).CurrentValue; - context.SaveChanges(); + await context.SaveChangesAsync(); Assert.False(entry.Property(e => e.Id).IsTemporary); Assert.NotEqual(tempValue, entry.Property(e => e.Id).CurrentValue); diff --git a/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs index 4765fe20e..bef3d8b23 100644 --- a/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class StoreGeneratedNpgsqlTest - : StoreGeneratedTestBase +public class StoreGeneratedNpgsqlTest(StoreGeneratedNpgsqlTest.StoreGeneratedNpgsqlFixture fixture) + : StoreGeneratedTestBase(fixture) { - public StoreGeneratedNpgsqlTest(StoreGeneratedNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs b/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs index 44f5c3500..cd7150436 100644 --- a/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs +++ b/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs @@ -30,13 +30,8 @@ public void Xmin() Assert.NotEqual(firstVersion, secondVersion); } - public class SystemColumnContext : PoolableDbContext + public class SystemColumnContext(DbContextOptions options) : PoolableDbContext(options) { - public SystemColumnContext(DbContextOptions options) - : base(options) - { - } - // ReSharper disable once UnusedAutoPropertyAccessor.Local public DbSet Entities { get; set; } diff --git a/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs index 941ce5c47..c89e21ae6 100644 --- a/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs @@ -2,13 +2,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class TPTTableSplittingNpgsqlTest : TPTTableSplittingTestBase +public class TPTTableSplittingNpgsqlTest(ITestOutputHelper testOutputHelper) : TPTTableSplittingTestBase(testOutputHelper) { - public TPTTableSplittingNpgsqlTest(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - public override Task Can_insert_dependent_with_just_one_parent() // This scenario is not valid for TPT => Task.CompletedTask; diff --git a/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs index 74c511879..d47776766 100644 --- a/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs @@ -4,13 +4,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; [MinimumPostgresVersion(12, 0)] // Test suite uses computed columns -public class TableSplittingNpgsqlTest : TableSplittingTestBase +public class TableSplittingNpgsqlTest(ITestOutputHelper testOutputHelper) : TableSplittingTestBase(testOutputHelper) { - public TableSplittingNpgsqlTest(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs index 3d4600386..c7dde4960 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs @@ -23,8 +23,10 @@ public class ArrayEntity public SomeEnum EnumConvertedToString { get; set; } public SomeEnum? NullableEnumConvertedToString { get; set; } public SomeEnum? NullableEnumConvertedToStringWithNonNullableLambda { get; set; } - public SomeEnum[] ValueConvertedArray { get; set; } = null!; - public List ValueConvertedList { get; set; } = null!; + public SomeEnum[] ValueConvertedArrayOfEnum { get; set; } = null!; + public List ValueConvertedListOfEnum { get; set; } = null!; + public string[] ArrayOfStringConvertedToDelimitedString { get; set; } = null!; + public List ListOfStringConvertedToDelimitedString { get; set; } = null!; public IList IList { get; set; } = null!; public byte Byte { get; set; } } diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs index 5b8cb32b0..389023fd6 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs @@ -1,15 +1,10 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; -public class ArrayQueryContext : PoolableDbContext +public class ArrayQueryContext(DbContextOptions options) : PoolableDbContext(options) { public DbSet SomeEntities { get; set; } public DbSet SomeEntityContainers { get; set; } - public ArrayQueryContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( e => @@ -32,23 +27,39 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) e.Property(ae => ae.NullableEnumConvertedToStringWithNonNullableLambda) .HasConversion(new ValueConverter(w => w.ToString(), v => Enum.Parse(v))); - e.PrimitiveCollection(ae => ae.ValueConvertedArray) + e.Property(ae => ae.ListOfStringConvertedToDelimitedString) + .HasConversion( + v => string.Join(",", v), + v => v.Split(',', StringSplitOptions.None).ToList(), + new ValueComparer>( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToList())); + + e.Property(ae => ae.ArrayOfStringConvertedToDelimitedString) + .HasConversion( + v => string.Join(",", v), + v => v.Split(',', StringSplitOptions.None).ToArray(), + new ValueComparer( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToArray())); + + e.PrimitiveCollection(ae => ae.ValueConvertedArrayOfEnum) .ElementType(eb => eb.HasConversion(typeof(EnumToStringConverter))); - e.PrimitiveCollection(ae => ae.ValueConvertedList) + e.PrimitiveCollection(ae => ae.ValueConvertedListOfEnum) .ElementType(eb => eb.HasConversion(typeof(EnumToStringConverter))); e.HasIndex(ae => ae.NonNullableText); }); - public static void Seed(ArrayQueryContext context) + public static async Task SeedAsync(ArrayQueryContext context) { var arrayEntities = ArrayQueryData.CreateArrayEntities(); context.SomeEntities.AddRange(arrayEntities); - context.SomeEntityContainers.Add( - new ArrayContainerEntity { Id = 1, ArrayEntities = arrayEntities.ToList() } - ); - context.SaveChanges(); + context.SomeEntityContainers.Add(new ArrayContainerEntity { Id = 1, ArrayEntities = arrayEntities.ToList() }); + await context.SaveChangesAsync(); } } diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs index 7b056188e..32e5a6c3a 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs @@ -2,13 +2,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; public class ArrayQueryData : ISetSource { - public IReadOnlyList ArrayEntities { get; } - public IReadOnlyList ContainerEntities { get; } - - public ArrayQueryData() - { - (ArrayEntities, ContainerEntities) = (CreateArrayEntities(), CreateContainerEntities()); - } + public IReadOnlyList ArrayEntities { get; } = CreateArrayEntities(); + public IReadOnlyList ContainerEntities { get; } = CreateContainerEntities(); public IQueryable Set() where TEntity : class @@ -27,31 +22,21 @@ public IQueryable Set() } public static IReadOnlyList CreateArrayEntities() - => new ArrayEntity[] - { + => + [ new() { Id = 1, - IntArray = new[] { 3, 4 }, - IntList = new List { 3, 4 }, - NullableIntArray = new int?[] { 3, 4, null }, - NullableIntList = new List - { - 3, - 4, - null - }, - Bytea = new byte[] { 3, 4 }, - ByteArray = new byte[] { 3, 4 }, - StringArray = new[] { "3", "4" }, - NullableStringArray = new[] { "3", "4", null }, - StringList = new List { "3", "4" }, - NullableStringList = new List - { - "3", - "4", - null - }, + IntArray = [3, 4], + IntList = [3, 4], + NullableIntArray = [3, 4, null], + NullableIntList = [3, 4, null], + Bytea = [3, 4], + ByteArray = [3, 4], + StringArray = ["3", "4"], + NullableStringArray = ["3", "4", null], + StringList = ["3", "4"], + NullableStringList = ["3", "4", null], NullableText = "foo", NonNullableText = "foo", Varchar10 = "foo", @@ -60,48 +45,26 @@ public static IReadOnlyList CreateArrayEntities() EnumConvertedToString = SomeEnum.One, NullableEnumConvertedToString = SomeEnum.One, NullableEnumConvertedToStringWithNonNullableLambda = SomeEnum.One, - ValueConvertedArray = new[] { SomeEnum.Eight, SomeEnum.Nine }, - ValueConvertedList = new List { SomeEnum.Eight, SomeEnum.Nine }, - IList = new[] { 8, 9 }, + ValueConvertedArrayOfEnum = [SomeEnum.Eight, SomeEnum.Nine], + ValueConvertedListOfEnum = [SomeEnum.Eight, SomeEnum.Nine], + ArrayOfStringConvertedToDelimitedString = ["3", "4"], + ListOfStringConvertedToDelimitedString = ["3", "4"], + IList = [8, 9], Byte = 10 }, new() { Id = 2, - IntArray = new[] { 5, 6, 7, 8 }, - IntList = new List - { - 5, - 6, - 7, - 8 - }, - NullableIntArray = new int?[] { 5, 6, 7, 8 }, - NullableIntList = new List - { - 5, - 6, - 7, - 8 - }, - Bytea = new byte[] { 5, 6, 7, 8 }, - ByteArray = new byte[] { 5, 6, 7, 8 }, - StringArray = new[] { "5", "6", "7", "8" }, - NullableStringArray = new[] { "5", "6", "7", "8" }, - StringList = new List - { - "5", - "6", - "7", - "8" - }, - NullableStringList = new List - { - "5", - "6", - "7", - "8" - }, + IntArray = [5, 6, 7, 8], + IntList = [5, 6, 7, 8], + NullableIntArray = [5, 6, 7, 8], + NullableIntList = [5, 6, 7, 8], + Bytea = [5, 6, 7, 8], + ByteArray = [5, 6, 7, 8], + StringArray = ["5", "6", "7", "8"], + NullableStringArray = ["5", "6", "7", "8"], + StringList = ["5", "6", "7", "8"], + NullableStringList = ["5", "6", "7", "8"], NullableText = "bar", NonNullableText = "bar", Varchar10 = "bar", @@ -110,13 +73,15 @@ public static IReadOnlyList CreateArrayEntities() EnumConvertedToString = SomeEnum.Two, NullableEnumConvertedToString = SomeEnum.Two, NullableEnumConvertedToStringWithNonNullableLambda = SomeEnum.Two, - ValueConvertedArray = new[] { SomeEnum.Nine, SomeEnum.Ten }, - ValueConvertedList = new List { SomeEnum.Nine, SomeEnum.Ten }, - IList = new[] { 9, 10 }, + ValueConvertedArrayOfEnum = [SomeEnum.Nine, SomeEnum.Ten], + ValueConvertedListOfEnum = [SomeEnum.Nine, SomeEnum.Ten], + ArrayOfStringConvertedToDelimitedString = ["5", "6", "7", "8"], + ListOfStringConvertedToDelimitedString = ["5", "6", "7", "8"], + IList = [9, 10], Byte = 20 } - }; + ]; public static IReadOnlyList CreateContainerEntities() - => new[] { new ArrayContainerEntity { Id = 1, ArrayEntities = CreateArrayEntities().ToList() } }; + => [new ArrayContainerEntity { Id = 1, ArrayEntities = CreateArrayEntities().ToList() }]; } diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs b/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs index b0a47a54f..fd80fe107 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs +++ b/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs @@ -2,13 +2,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Northwind; -public class NorthwindNpgsqlContext : NorthwindRelationalContext +public class NorthwindNpgsqlContext(DbContextOptions options) : NorthwindRelationalContext(options) { - public NorthwindNpgsqlContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs index e7d1f56f7..86e224e7d 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs @@ -1,14 +1,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -public sealed class MinimumPostgresVersionAttribute : Attribute, ITestCondition +public sealed class MinimumPostgresVersionAttribute(int major, int minor) : Attribute, ITestCondition { - private readonly Version _version; - - public MinimumPostgresVersionAttribute(int major, int minor) - { - _version = new Version(major, minor); - } + private readonly Version _version = new(major, minor); public ValueTask IsMetAsync() => new(TestEnvironment.PostgresVersion >= _version); diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs index a1805342a..5698435e0 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs @@ -9,12 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; public class NpgsqlDatabaseCleaner : RelationalDatabaseCleaner { - private readonly NpgsqlSqlGenerationHelper _sqlGenerationHelper; - - public NpgsqlDatabaseCleaner() - { - _sqlGenerationHelper = new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()); - } + private readonly NpgsqlSqlGenerationHelper _sqlGenerationHelper = new(new RelationalSqlGenerationHelperDependencies()); protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) => new NpgsqlDatabaseModelFactory( @@ -58,8 +53,7 @@ public override void Clean(DatabaseFacade facade) private void DropExtensions(NpgsqlConnection conn) { - const string getExtensions = @" -SELECT name FROM pg_available_extensions WHERE installed_version IS NOT NULL AND name <> 'plpgsql'"; + const string getExtensions = "SELECT name FROM pg_available_extensions WHERE installed_version IS NOT NULL AND name <> 'plpgsql'"; List extensions; using (var cmd = new NpgsqlCommand(getExtensions, conn)) @@ -81,11 +75,12 @@ private void DropExtensions(NpgsqlConnection conn) /// private void DropTypes(NpgsqlConnection conn) { - const string getUserDefinedRangesEnums = @" + const string getUserDefinedRangesEnums = """ SELECT ns.nspname, typname FROM pg_type JOIN pg_namespace AS ns ON ns.oid = pg_type.typnamespace -WHERE typtype IN ('r', 'e') AND nspname <> 'pg_catalog'"; +WHERE typtype IN ('r', 'e') AND nspname <> 'pg_catalog' +"""; (string Schema, string Name)[] userDefinedTypes; using (var cmd = new NpgsqlCommand(getUserDefinedRangesEnums, conn)) @@ -96,7 +91,7 @@ FROM pg_type if (userDefinedTypes.Any()) { - var dropTypes = string.Concat(userDefinedTypes.Select(t => $@"DROP TYPE ""{t.Schema}"".""{t.Name}"" CASCADE;")); + var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP TYPE "{t.Schema}"."{t.Name}" CASCADE;""")); using var cmd = new NpgsqlCommand(dropTypes, conn); cmd.ExecuteNonQuery(); } @@ -107,8 +102,8 @@ FROM pg_type /// private void DropFunctions(NpgsqlConnection conn) { - const string getUserDefinedFunctions = @" -SELECT 'DROP ROUTINE ""' || nspname || '"".""' || proname || '""(' || oidvectortypes(proargtypes) || ');' FROM pg_proc + const string getUserDefinedFunctions = """ +SELECT 'DROP ROUTINE "' || nspname || '"."' || proname || '"(' || oidvectortypes(proargtypes) || ');' FROM pg_proc JOIN pg_namespace AS ns ON ns.oid = pg_proc.pronamespace WHERE nspname NOT IN ('pg_catalog', 'information_schema') AND @@ -116,7 +111,8 @@ NOT EXISTS ( SELECT * FROM pg_depend AS dep WHERE dep.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_proc') AND dep.objid = pg_proc.oid AND - deptype = 'e');"; + deptype = 'e'); +"""; string dropSql; using (var cmd = new NpgsqlCommand(getUserDefinedFunctions, conn)) @@ -139,11 +135,13 @@ private void DropCollations(NpgsqlConnection conn) return; } - const string getUserCollations = @"SELECT nspname, collname + const string getUserCollations = + """ +SELECT nspname, collname FROM pg_collation coll JOIN pg_namespace ns ON ns.oid=coll.collnamespace - JOIN pg_authid auth ON auth.oid = coll.collowner WHERE rolname <> 'postgres'; -"; + JOIN pg_authid auth ON auth.oid = coll.collowner WHERE nspname <> 'pg_catalog'; +"""; (string Schema, string Name)[] userDefinedTypes; using (var cmd = new NpgsqlCommand(getUserCollations, conn)) @@ -154,7 +152,7 @@ FROM pg_collation coll if (userDefinedTypes.Any()) { - var dropTypes = string.Concat(userDefinedTypes.Select(t => $@"DROP COLLATION ""{t.Schema}"".""{t.Name}"" CASCADE;")); + var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP COLLATION "{t.Schema}"."{t.Name}" CASCADE;""")); using var cmd = new NpgsqlCommand(dropTypes, conn); cmd.ExecuteNonQuery(); } 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/NpgsqlPrecompiledQueryTestHelpers.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs new file mode 100644 index 000000000..104096b41 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +public class NpgsqlPrecompiledQueryTestHelpers : PrecompiledQueryTestHelpers +{ + public static NpgsqlPrecompiledQueryTestHelpers Instance = new(); + + protected override IEnumerable BuildProviderMetadataReferences() + { + yield return MetadataReference.CreateFromFile(typeof(NpgsqlOptionsExtension).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(NpgsqlConnection).Assembly.Location); + yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs index 84b0e0a29..8b4ba0381 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs @@ -1,13 +1,17 @@ using System.Data; using System.Data.Common; using System.Text.RegularExpressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +#nullable enable + public class NpgsqlTestStore : RelationalTestStore { - private readonly string _scriptPath; - private readonly string _additionalSql; + private readonly string? _scriptPath; + private readonly string? _additionalSql; + private readonly string? _connectionString; private const string Northwind = "Northwind"; @@ -15,69 +19,72 @@ public class NpgsqlTestStore : RelationalTestStore public static readonly string NorthwindConnectionString = CreateConnectionString(Northwind); - public static NpgsqlTestStore GetNorthwindStore() - => (NpgsqlTestStore)NpgsqlNorthwindTestStoreFactory.Instance - .GetOrCreate(NpgsqlNorthwindTestStoreFactory.Name).Initialize(null, (Func)null); + public static async Task GetNorthwindStoreAsync() + => (NpgsqlTestStore)await NpgsqlNorthwindTestStoreFactory.Instance + .GetOrCreate(NpgsqlNorthwindTestStoreFactory.Name).InitializeAsync(null, (Func?)null); - // ReSharper disable once UnusedMember.Global - public static NpgsqlTestStore GetOrCreateInitialized(string name) - => new NpgsqlTestStore(name).InitializeNpgsql(null, (Func)null, null); + public static Task GetOrCreateInitializedAsync(string name) + => new NpgsqlTestStore(name).InitializeNpgsqlAsync(null, (Func?)null, null); public static NpgsqlTestStore GetOrCreate( string name, - string scriptPath = null, - string additionalSql = null, - string connectionStringOptions = null) - => new(name, scriptPath, additionalSql, connectionStringOptions); + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, + bool useConnectionString = false) + => new(name, scriptPath, additionalSql, connectionStringOptions, useConnectionString: useConnectionString); - public static NpgsqlTestStore Create(string name, string connectionStringOptions = null) + public static NpgsqlTestStore Create(string name, string? connectionStringOptions = null) => new(name, connectionStringOptions: connectionStringOptions, shared: false); - public static NpgsqlTestStore CreateInitialized(string name) - => new NpgsqlTestStore(name, shared: false) - .InitializeNpgsql(null, (Func)null, null); + public static Task CreateInitializedAsync(string name) + => new NpgsqlTestStore(name, shared: false).InitializeNpgsqlAsync(null, (Func?)null, null); - private NpgsqlTestStore( + public NpgsqlTestStore( string name, - string scriptPath = null, - string additionalSql = null, - string connectionStringOptions = null, - bool shared = true) - : base(name, shared) + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, + 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) { // ReSharper disable once AssignNullToNotNullAttribute - _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(NpgsqlTestStore).GetTypeInfo().Assembly.Location), scriptPath); + _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(NpgsqlTestStore).GetTypeInfo().Assembly.Location)!, scriptPath); } _additionalSql = additionalSql; - - // ReSharper disable VirtualMemberCallInConstructor - ConnectionString = CreateConnectionString(Name, connectionStringOptions); - Connection = new NpgsqlConnection(ConnectionString); - // ReSharper restore VirtualMemberCallInConstructor } + private static NpgsqlConnection CreateConnection(string name, string? connectionStringOptions) + => new(CreateConnectionString(name, connectionStringOptions)); + // ReSharper disable once MemberCanBePrivate.Global - public NpgsqlTestStore InitializeNpgsql( - IServiceProvider serviceProvider, - Func createContext, - Action seed) - => (NpgsqlTestStore)Initialize(serviceProvider, createContext, seed); + public async Task InitializeNpgsqlAsync( + IServiceProvider? serviceProvider, + Func? createContext, + Func? seed) + => (NpgsqlTestStore)await InitializeAsync(serviceProvider, createContext, seed); // ReSharper disable once UnusedMember.Global - public NpgsqlTestStore InitializeNpgsql( + public async Task InitializeNpgsqlAsync( IServiceProvider serviceProvider, Func createContext, - Action seed) - => InitializeNpgsql(serviceProvider, () => createContext(this), seed); + Func seed) + => await InitializeNpgsqlAsync(serviceProvider, () => createContext(this), seed); - protected override void Initialize(Func createContext, Action seed, Action clean) + protected override async Task InitializeAsync(Func createContext, Func? seed, Func? clean) { - if (CreateDatabase(clean)) + if (await CreateDatabaseAsync(clean)) { if (_scriptPath is not null) { @@ -90,73 +97,63 @@ protected override void Initialize(Func createContext, Action command.ExecuteNonQuery(), _additionalSql); } - seed?.Invoke(context); + if (seed is not null) + { + await seed(context); + } } } } public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) - => 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) + private async Task CreateDatabaseAsync(Func? clean) { - using (var master = new NpgsqlConnection(CreateAdminConnectionString())) + await using var master = new NpgsqlConnection(CreateAdminConnectionString()); + + if (await DatabaseExistsAsync(Name)) { - if (DatabaseExists(Name)) + if (_scriptPath is not null) { - if (_scriptPath is not null) - { - return false; - } - - using (var context = new DbContext( - AddProviderOptions( - new DbContextOptionsBuilder() - .EnableServiceProviderCaching(false)) - .Options)) - { - clean?.Invoke(context); - Clean(context); - return true; - } + return false; } - ExecuteNonQuery(master, GetCreateDatabaseStatement(Name)); - WaitForExists((NpgsqlConnection)Connection); + await using var context = new DbContext( + AddProviderOptions(new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + clean?.Invoke(context); + await CleanAsync(context); + return true; } + await ExecuteNonQueryAsync(master, GetCreateDatabaseStatement(Name)); + await WaitForExistsAsync((NpgsqlConnection)Connection); + return true; } - private static void WaitForExists(NpgsqlConnection connection) - => WaitForExistsImplementation(connection); - - private static void WaitForExistsImplementation(NpgsqlConnection connection) + private static async Task WaitForExistsAsync(NpgsqlConnection connection) { var retryCount = 0; while (true) @@ -165,13 +162,13 @@ private static void WaitForExistsImplementation(NpgsqlConnection connection) { if (connection.State != ConnectionState.Closed) { - connection.Close(); + await connection.CloseAsync(); } NpgsqlConnection.ClearPool(connection); - connection.Open(); - connection.Close(); + await connection.OpenAsync(); + await connection.CloseAsync(); return; } catch (PostgresException e) @@ -182,7 +179,7 @@ private static void WaitForExistsImplementation(NpgsqlConnection connection) throw; } - Thread.Sleep(100); + await Task.Delay(100); } } } @@ -206,61 +203,46 @@ public void ExecuteScript(string scriptPath) }, ""); } - // ReSharper disable once UnusedMember.Local - private static void Clean(string name) - { - var options = new DbContextOptionsBuilder() - .UseNpgsql(CreateConnectionString(name), b => b.ApplyConfiguration()) - .UseInternalServiceProvider( - new ServiceCollection() - .AddEntityFrameworkNpgsql() - .BuildServiceProvider()) - .Options; - - using (var context = new DbContext(options)) - { - context.Database.EnsureClean(); - } - } - private static string GetCreateDatabaseStatement(string name) - => $@"CREATE DATABASE ""{name}"""; + => $""" + CREATE DATABASE "{name}" + """; - private static bool DatabaseExists(string name) + private static async Task DatabaseExistsAsync(string name) { - using (var master = new NpgsqlConnection(CreateAdminConnectionString())) - { - return ExecuteScalar(master, $@"SELECT COUNT(*) FROM pg_database WHERE datname = '{name}'") > 0; - } + await using var master = new NpgsqlConnection(CreateAdminConnectionString()); + + return await ExecuteScalarAsync(master, $@"SELECT COUNT(*) FROM pg_database WHERE datname = '{name}'") > 0; } - public void DeleteDatabase() + public async Task DeleteDatabaseAsync() { - if (!DatabaseExists(Name)) + if (!await DatabaseExistsAsync(Name)) { return; } - using (var master = new NpgsqlConnection(CreateAdminConnectionString())) - { - ExecuteNonQuery(master, GetDisconnectDatabaseSql(Name)); - ExecuteNonQuery(master, GetDropDatabaseSql(Name)); + await using var master = new NpgsqlConnection(CreateAdminConnectionString()); - NpgsqlConnection.ClearAllPools(); - } + await ExecuteNonQueryAsync(master, GetDisconnectDatabaseSql(Name)); + await ExecuteNonQueryAsync(master, GetDropDatabaseSql(Name)); + + NpgsqlConnection.ClearAllPools(); } // Kill all connection to the database - // TODO: Pre-9.2 PG has column name procid instead of pid private static string GetDisconnectDatabaseSql(string name) - => $@" -REVOKE CONNECT ON DATABASE ""{name}"" FROM PUBLIC; + => $""" +REVOKE CONNECT ON DATABASE "{name}" FROM PUBLIC; SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity - WHERE datname = '{name}'"; + WHERE datname = '{name}' +"""; private static string GetDropDatabaseSql(string name) - => $@"DROP DATABASE ""{name}"""; + => $""" + DROP DATABASE "{name}" + """; public override void OpenConnection() => Connection.Open(); @@ -273,67 +255,65 @@ public T ExecuteScalar(string sql, params object[] parameters) => ExecuteScalar(Connection, sql, parameters); private static T ExecuteScalar(DbConnection connection, string sql, params object[] parameters) - => Execute(connection, command => (T)command.ExecuteScalar(), sql, false, parameters); + => Execute(connection, command => (T)command.ExecuteScalar()!, sql, false, parameters); // ReSharper disable once UnusedMember.Global public Task ExecuteScalarAsync(string sql, params object[] parameters) => ExecuteScalarAsync(Connection, sql, parameters); - private static Task ExecuteScalarAsync(DbConnection connection, string sql, object[] parameters = null) - => ExecuteAsync(connection, async command => (T)await command.ExecuteScalarAsync(), sql, false, parameters); + private static Task ExecuteScalarAsync(DbConnection connection, string sql, object[]? parameters = null) + => ExecuteAsync(connection, async command => (T)(await command.ExecuteScalarAsync())!, sql, false, parameters); // ReSharper disable once UnusedMethodReturnValue.Global public int ExecuteNonQuery(string sql, params object[] parameters) => ExecuteNonQuery(Connection, sql, parameters); - private static int ExecuteNonQuery(DbConnection connection, string sql, object[] parameters = null) + private static int ExecuteNonQuery(DbConnection connection, string sql, object[]? parameters = null) => Execute(connection, command => command.ExecuteNonQuery(), sql, false, parameters); // ReSharper disable once UnusedMember.Global public Task ExecuteNonQueryAsync(string sql, params object[] parameters) => ExecuteNonQueryAsync(Connection, sql, parameters); - private static Task ExecuteNonQueryAsync(DbConnection connection, string sql, object[] parameters = null) + private static Task ExecuteNonQueryAsync(DbConnection connection, string sql, object[]? parameters = null) => ExecuteAsync(connection, command => command.ExecuteNonQueryAsync(), sql, false, parameters); // ReSharper disable once UnusedMember.Global public IEnumerable Query(string sql, params object[] parameters) => Query(Connection, sql, parameters); - private static IEnumerable Query(DbConnection connection, string sql, object[] parameters = null) + private static IEnumerable Query(DbConnection connection, string sql, object[]? parameters = null) => Execute( connection, command => { - using (var dataReader = command.ExecuteReader()) - { - var results = Enumerable.Empty(); - while (dataReader.Read()) - { - results = results.Concat(new[] { dataReader.GetFieldValue(0) }); - } + using var dataReader = command.ExecuteReader(); - return results; + var results = Enumerable.Empty(); + while (dataReader.Read()) + { + results = results.Concat([dataReader.GetFieldValue(0)]); } + + return results; }, sql, false, parameters); // ReSharper disable once UnusedMember.Global public Task> QueryAsync(string sql, params object[] parameters) => QueryAsync(Connection, sql, parameters); - private static Task> QueryAsync(DbConnection connection, string sql, object[] parameters = null) + private static Task> QueryAsync(DbConnection connection, string sql, object[]? parameters = null) => ExecuteAsync( connection, async command => { - await using (var dataReader = await command.ExecuteReaderAsync()) - { - var results = Enumerable.Empty(); - while (await dataReader.ReadAsync()) - { - results = results.Concat(new[] { await dataReader.GetFieldValueAsync(0) }); - } + await using var dataReader = await command.ExecuteReaderAsync(); - return results; + var results = Enumerable.Empty(); + while (await dataReader.ReadAsync()) + { + results = results.Concat([await dataReader.GetFieldValueAsync(0)]); } + + return results; }, sql, false, parameters); private static T Execute( @@ -341,7 +321,7 @@ private static T Execute( Func execute, string sql, bool useTransaction = false, - object[] parameters = null) + object[]? parameters = null) => ExecuteCommand(connection, execute, sql, useTransaction, parameters); private static T ExecuteCommand( @@ -349,7 +329,7 @@ private static T ExecuteCommand( Func execute, string sql, bool useTransaction, - object[] parameters) + object[]? parameters) { if (connection.State != ConnectionState.Closed) { @@ -359,19 +339,18 @@ private static T ExecuteCommand( connection.Open(); try { - using (var transaction = useTransaction ? connection.BeginTransaction() : null) + using var transaction = useTransaction ? connection.BeginTransaction() : null; + + T result; + using (var command = CreateCommand(connection, sql, parameters)) { - T result; - using (var command = CreateCommand(connection, sql, parameters)) - { - command.Transaction = transaction; - result = execute(command); - } + command.Transaction = transaction; + result = execute(command); + } - transaction?.Commit(); + transaction?.Commit(); - return result; - } + return result; } finally { @@ -388,7 +367,7 @@ private static Task ExecuteAsync( Func> executeAsync, string sql, bool useTransaction = false, - IReadOnlyList parameters = null) + IReadOnlyList? parameters = null) => ExecuteCommandAsync(connection, executeAsync, sql, useTransaction, parameters); private static async Task ExecuteCommandAsync( @@ -396,35 +375,37 @@ private static async Task ExecuteCommandAsync( Func> executeAsync, string sql, bool useTransaction, - IReadOnlyList parameters) + IReadOnlyList? parameters) { if (connection.State != ConnectionState.Closed) { - connection.Close(); + await connection.CloseAsync(); } await connection.OpenAsync(); try { - await using (var transaction = useTransaction ? connection.BeginTransaction() : null) - { - T result; - await using (var command = CreateCommand(connection, sql, parameters)) - { - result = await executeAsync(command); - } + await using var transaction = useTransaction ? await connection.BeginTransactionAsync() : null; - transaction?.Commit(); + T result; + await using (var command = CreateCommand(connection, sql, parameters)) + { + result = await executeAsync(command); + } - return result; + if (transaction is not null) + { + await transaction.CommitAsync(); } + + return result; } finally { if (connection.State == ConnectionState.Closed && connection.State != ConnectionState.Closed) { - connection.Close(); + await connection.CloseAsync(); } } } @@ -432,7 +413,7 @@ private static async Task ExecuteCommandAsync( private static DbCommand CreateCommand( DbConnection connection, string commandText, - IReadOnlyList parameters = null) + IReadOnlyList? parameters = null) { var command = (NpgsqlCommand)connection.CreateCommand(); @@ -450,7 +431,7 @@ private static DbCommand CreateCommand( return command; } - public static string CreateConnectionString(string name, string options = null) + public static string CreateConnectionString(string name, string? options = null) { var builder = new NpgsqlConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = name }; @@ -465,6 +446,9 @@ public static string CreateConnectionString(string name, string options = null) private static string CreateAdminConnectionString() => CreateConnectionString("postgres"); - public override void Clean(DbContext context) - => context.Database.EnsureClean(); + public override Task CleanAsync(DbContext context) + { + context.Database.EnsureClean(); + return Task.CompletedTask; + } } diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs index 1f6858fbb..06de22f29 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs @@ -1,24 +1,20 @@ 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(); diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs index 5a2a46057..15ef578c7 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -public class TestNpgsqlConnection : NpgsqlRelationalConnection +public class TestNpgsqlConnection(RelationalConnectionDependencies dependencies, DbDataSource dataSource = null) + : NpgsqlRelationalConnection(dependencies, dataSource) { - public TestNpgsqlConnection(RelationalConnectionDependencies dependencies, DbDataSource dataSource = null) - : base(dependencies, dataSource) - { - } - public string ErrorCode { get; set; } = "XX000"; public Queue OpenFailures { get; } = new(); public int OpenCount { get; set; } diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs index 0c3c982f8..c443ccdc8 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs @@ -4,7 +4,7 @@ public class TestNpgsqlRetryingExecutionStrategy : NpgsqlRetryingExecutionStrate { private const bool ErrorNumberDebugMode = false; - private static readonly string[] AdditionalSqlStates = { "XX000" }; + private static readonly string[] AdditionalSqlStates = ["XX000"]; public TestNpgsqlRetryingExecutionStrategy() : base( diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs index 1746cd7dd..6dcc65fd1 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs @@ -2,32 +2,20 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -public class TestRelationalCommandBuilderFactory : IRelationalCommandBuilderFactory +public class TestRelationalCommandBuilderFactory(RelationalCommandBuilderDependencies dependencies) : IRelationalCommandBuilderFactory { - public TestRelationalCommandBuilderFactory( - RelationalCommandBuilderDependencies dependencies) - { - Dependencies = dependencies; - } - - public RelationalCommandBuilderDependencies Dependencies { get; } + public RelationalCommandBuilderDependencies Dependencies { get; } = dependencies; public virtual IRelationalCommandBuilder Create() => new TestRelationalCommandBuilder(Dependencies); - private class TestRelationalCommandBuilder : IRelationalCommandBuilder + private class TestRelationalCommandBuilder(RelationalCommandBuilderDependencies dependencies) : IRelationalCommandBuilder { - private readonly List _parameters = new(); - - public TestRelationalCommandBuilder( - RelationalCommandBuilderDependencies dependencies) - { - Dependencies = dependencies; - } + private readonly List _parameters = []; public IndentedStringBuilder Instance { get; } = new(); - public RelationalCommandBuilderDependencies Dependencies { get; } + public RelationalCommandBuilderDependencies Dependencies { get; } = dependencies; public IReadOnlyList Parameters => _parameters; @@ -88,17 +76,13 @@ public int CommandTextLength => Instance.Length; } - private class TestRelationalCommand : IRelationalCommand + private class TestRelationalCommand( + RelationalCommandBuilderDependencies dependencies, + string commandText, + IReadOnlyList parameters) + : IRelationalCommand { - private readonly RelationalCommand _realRelationalCommand; - - public TestRelationalCommand( - RelationalCommandBuilderDependencies dependencies, - string commandText, - IReadOnlyList parameters) - { - _realRelationalCommand = new RelationalCommand(dependencies, commandText, parameters); - } + private readonly RelationalCommand _realRelationalCommand = new(dependencies, commandText, parameters); public string CommandText => _realRelationalCommand.CommandText; diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs index 9cfe34475..89cfb1b93 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs @@ -2,14 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -public class TestRelationalTransactionFactory : IRelationalTransactionFactory +public class TestRelationalTransactionFactory(RelationalTransactionFactoryDependencies dependencies) : IRelationalTransactionFactory { - public TestRelationalTransactionFactory(RelationalTransactionFactoryDependencies dependencies) - { - Dependencies = dependencies; - } - - protected virtual RelationalTransactionFactoryDependencies Dependencies { get; } + protected virtual RelationalTransactionFactoryDependencies Dependencies { get; } = dependencies; public RelationalTransaction Create( IRelationalConnection connection, @@ -20,20 +15,15 @@ public RelationalTransaction Create( => new TestRelationalTransaction(connection, transaction, logger, transactionOwned, Dependencies.SqlGenerationHelper); } -public class TestRelationalTransaction : RelationalTransaction +public class TestRelationalTransaction( + IRelationalConnection connection, + DbTransaction transaction, + IDiagnosticsLogger logger, + bool transactionOwned, + ISqlGenerationHelper sqlGenerationHelper) + : RelationalTransaction(connection, transaction, new Guid(), logger, transactionOwned, sqlGenerationHelper) { - private readonly TestNpgsqlConnection _testConnection; - - public TestRelationalTransaction( - IRelationalConnection connection, - DbTransaction transaction, - IDiagnosticsLogger logger, - bool transactionOwned, - ISqlGenerationHelper sqlGenerationHelper) - : base(connection, transaction, new Guid(), logger, transactionOwned, sqlGenerationHelper) - { - _testConnection = (TestNpgsqlConnection)connection; - } + private readonly TestNpgsqlConnection _testConnection = (TestNpgsqlConnection)connection; public override void Commit() { diff --git a/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs index 05b57061c..7fe19ba50 100644 --- a/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public abstract class TransactionInterceptionNpgsqlTestBase : TransactionInterceptionTestBase +public abstract class TransactionInterceptionNpgsqlTestBase(TransactionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) + : TransactionInterceptionTestBase(fixture) { - protected TransactionInterceptionNpgsqlTestBase(InterceptionNpgsqlFixtureBase fixture) - : base(fixture) - { - } - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase { protected override string StoreName @@ -23,14 +19,9 @@ protected override IServiceCollection InjectInterceptors( => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); } - public class TransactionInterceptionNpgsqlTest - : TransactionInterceptionNpgsqlTestBase, IClassFixture + public class TransactionInterceptionNpgsqlTest(TransactionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) + : TransactionInterceptionNpgsqlTestBase(fixture), IClassFixture { - public TransactionInterceptionNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener @@ -38,14 +29,11 @@ protected override bool ShouldSubscribeToDiagnosticListener } } - public class TransactionInterceptionWithDiagnosticsNpgsqlTest - : TransactionInterceptionNpgsqlTestBase, IClassFixture + public class TransactionInterceptionWithDiagnosticsNpgsqlTest( + TransactionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) + : TransactionInterceptionNpgsqlTestBase(fixture), + IClassFixture { - public TransactionInterceptionWithDiagnosticsNpgsqlTest(InterceptionNpgsqlFixture fixture) - : base(fixture) - { - } - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener diff --git a/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs index a80111f5b..0eb3c9bc3 100644 --- a/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs @@ -4,13 +4,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class TransactionNpgsqlTest : TransactionTestBase +public class TransactionNpgsqlTest(TransactionNpgsqlTest.TransactionNpgsqlFixture fixture) + : TransactionTestBase(fixture) { - public TransactionNpgsqlTest(TransactionNpgsqlFixture fixture) - : base(fixture) - { - } - public override Task SaveChanges_can_be_used_with_AutoTransactionBehavior_Never(bool async) // Npgsql batches the inserts, creating an implicit transaction which fails the test // (see https://github.com/npgsql/npgsql/issues/1307) diff --git a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs index 96ced3bd3..9a6ab976a 100644 --- a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs @@ -2,13 +2,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class TwoDatabasesNpgsqlTest : TwoDatabasesTestBase, IClassFixture +public class TwoDatabasesNpgsqlTest(NpgsqlFixture fixture) : TwoDatabasesTestBase(fixture), IClassFixture { - public TwoDatabasesNpgsqlTest(NpgsqlFixture fixture) - : base(fixture) - { - } - protected new NpgsqlFixture Fixture => (NpgsqlFixture)base.Fixture; diff --git a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs index 7a555425c..e3c8a983a 100644 --- a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs @@ -18,7 +18,7 @@ public override async Task Add_element_to_json_collection_branch() AssertSql( """ -@p0='[{"Date":"2101-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":10.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":10.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}},{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}]' (Nullable = false) (DbType = Object) +@p0='[{"Date":"2101-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":10.1,"Id":89,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":10.2,"Id":90,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}},{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":77,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}]' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch}', @p0) @@ -58,7 +58,7 @@ public override async Task Add_element_to_json_collection_on_derived() AssertSql( """ -@p0='[{"Date":"2221-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":221.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"d2_r_c1"},{"SomethingSomething":"d2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"d2_r_r"}},{"Date":"2222-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":222.1,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"d2_r_c1"},{"SomethingSomething":"d2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"d2_r_r"}},{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}]' (Nullable = false) (DbType = Object) +@p0='[{"Date":"2221-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":221.1,"Id":104,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"d2_r_c1"},{"SomethingSomething":"d2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"d2_r_r"}},{"Date":"2222-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":222.1,"Id":105,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"d2_r_c1"},{"SomethingSomething":"d2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"d2_r_r"}},{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":77,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}]' (Nullable = false) (DbType = Object) @p1='2' UPDATE "JsonEntitiesInheritance" SET "CollectionOnDerived" = @p0 @@ -79,7 +79,7 @@ public override async Task Add_element_to_json_collection_root() AssertSql( """ -@p0='[{"Name":"e1_c1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":11.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":11.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":11.0,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Name":"e1_c2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":12.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":12.2,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":12.0,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}},{"Name":"new Name","Names":null,"Number":142,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}]' (Nullable = false) (DbType = Object) +@p0='[{"Id":0,"Name":"e1_c1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":11.1,"Id":92,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":11.2,"Id":93,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":11.0,"Id":91,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Id":0,"Name":"e1_c2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":12.1,"Id":95,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":12.2,"Id":96,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":12.0,"Id":94,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}},{"Id":0,"Name":"new Name","Names":null,"Number":142,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":7,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}]' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 @@ -99,7 +99,7 @@ public override async Task Add_element_to_json_collection_root_null_navigations( AssertSql( """ -@p0='[{"Name":"e1_c1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":11.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":11.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":11.0,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Name":"e1_c2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":12.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":12.2,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":12.0,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}},{"Name":"new Name","Names":null,"Number":142,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}}]' (Nullable = false) (DbType = Object) +@p0='[{"Id":0,"Name":"e1_c1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":11.1,"Id":92,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":11.2,"Id":93,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":11.0,"Id":91,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Id":0,"Name":"e1_c2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":12.1,"Id":95,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":12.2,"Id":96,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":12.0,"Id":94,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}},{"Id":0,"Name":"new Name","Names":null,"Number":142,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":7,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":null}}]' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 @@ -119,7 +119,7 @@ public override async Task Add_entity_with_json() AssertSql( """ -@p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (DbType = Object) +@p0='{"Id":0,"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":7,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (DbType = Object) @p1='[]' (Nullable = false) (DbType = Object) @p2='2' @p3=NULL (DbType = Int32) @@ -141,7 +141,7 @@ public override async Task Add_entity_with_json_null_navigations() AssertSql( """ -@p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":null}}' (Nullable = false) (DbType = Object) +@p0='{"Id":0,"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":null,"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":7,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":null}}' (Nullable = false) (DbType = Object) @p1='2' @p2=NULL (DbType = Int32) @p3='NewEntity' @@ -182,7 +182,7 @@ public override async Task Add_json_reference_root() AssertSql( """ -@p0='{"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":2,"Enums":null,"Fraction":42.42,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (DbType = Object) +@p0='{"Id":0,"Name":"RootName","Names":null,"Number":42,"Numbers":null,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2010-10-10T00:00:00","Enum":-3,"Enums":null,"Fraction":42.42,"Id":7,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":[{"SomethingSomething":"ss1"},{"SomethingSomething":"ss2"}],"OwnedReferenceLeaf":{"SomethingSomething":"ss3"}}}' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = @p0 @@ -360,8 +360,8 @@ public override async Task Edit_element_in_json_multiple_levels_partial_update() AssertSql( """ -@p0='[{"Date":"2111-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":11.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"...and another"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":11.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"yet another change"},{"SomethingSomething":"and another"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}]' (Nullable = false) (DbType = Object) -@p1='{"Name":"edit","Names":["e1_r1","e1_r2"],"Number":10,"Numbers":[-2147483648,-1,0,1,2147483647],"OwnedCollectionBranch":[{"Date":"2101-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":10.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":10.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}}],"OwnedReferenceBranch":{"Date":"2111-11-11T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":10.0,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_r_r"}}}' (Nullable = false) (DbType = Object) +@p0='[{"Date":"2111-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":11.1,"Id":92,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"...and another"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":11.2,"Id":93,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"yet another change"},{"SomethingSomething":"and another"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}]' (Nullable = false) (DbType = Object) +@p1='{"Id":0,"Name":"edit","Names":["e1_r1","e1_r2"],"Number":10,"Numbers":[-2147483648,-1,0,1,2147483647],"OwnedCollectionBranch":[{"Date":"2101-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":10.1,"Id":89,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":10.2,"Id":90,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}}],"OwnedReferenceBranch":{"Date":"2111-11-11T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":10.0,"Id":88,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_r_r"}}}' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{0,OwnedCollectionBranch}', @p0), "OwnedReferenceRoot" = @p1 @@ -381,7 +381,7 @@ public override async Task Edit_element_in_json_branch_collection_and_add_elemen AssertSql( """ -@p0='[{"Date":"2101-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":4321.3,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":10.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}},{"Date":"2222-11-11T00:00:00","Enum":2,"Enums":null,"Fraction":45.32,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":{"SomethingSomething":"cc"}}]' (Nullable = false) (DbType = Object) +@p0='[{"Date":"2101-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":4321.3,"Id":89,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c1_c1"},{"SomethingSomething":"e1_r_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c1_r"}},{"Date":"2102-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":10.2,"Id":90,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_c2_c1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_c2_r"}},{"Date":"2222-11-11T00:00:00","Enum":-3,"Enums":null,"Fraction":45.32,"Id":77,"NullableEnum":null,"NullableEnums":null,"OwnedCollectionLeaf":null,"OwnedReferenceLeaf":{"SomethingSomething":"cc"}}]' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch}', @p0) @@ -421,7 +421,7 @@ public override async Task Edit_two_elements_in_the_same_json_collection_at_the_ AssertSql( """ -@p0='[{"Name":"edit1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":11.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":11.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":11.0,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Name":"edit2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":1,"Enums":[0,-1,1],"Fraction":12.1,"NullableEnum":0,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":12.2,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":12.0,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}}]' (Nullable = false) (DbType = Object) +@p0='[{"Id":0,"Name":"edit1","Names":["e1_c11","e1_c12"],"Number":11,"Numbers":[-1000,0,1000],"OwnedCollectionBranch":[{"Date":"2111-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":11.1,"Id":92,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c1_c1"},{"SomethingSomething":"e1_c1_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c1_r"}},{"Date":"2112-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":11.2,"Id":93,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_c2_c1"},{"SomethingSomething":"e1_c1_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_c2_r"}}],"OwnedReferenceBranch":{"Date":"2110-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":11.0,"Id":91,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c1_r_c1"},{"SomethingSomething":"e1_c1_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c1_r_r"}}},{"Id":0,"Name":"edit2","Names":["e1_c21","e1_c22"],"Number":12,"Numbers":[-1001,0,1001],"OwnedCollectionBranch":[{"Date":"2121-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":12.1,"Id":95,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c1_c1"},{"SomethingSomething":"e1_c2_c1_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c1_r"}},{"Date":"2122-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":12.2,"Id":96,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_c2_c1"},{"SomethingSomething":"e1_c2_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_c2_r"}}],"OwnedReferenceBranch":{"Date":"2120-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":12.0,"Id":94,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_c2_r_c1"},{"SomethingSomething":"e1_c2_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_c2_r_r"}}}]' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 @@ -441,7 +441,7 @@ public override async Task Edit_collection_element_and_reference_at_once() AssertSql( """ -@p0='{"Date":"2102-01-01T00:00:00","Enum":2,"Enums":[0,-1,1],"Fraction":10.2,"NullableEnum":1,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"edit1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit2"}}' (Nullable = false) (DbType = Object) +@p0='{"Date":"2102-01-01T00:00:00","Enum":-3,"Enums":[-1,-1,2],"Fraction":10.2,"Id":90,"NullableEnum":2,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"edit1"},{"SomethingSomething":"e1_r_c2_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit2"}}' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch,1}', @p0) @@ -461,8 +461,8 @@ public override async Task Edit_single_enum_property() AssertSql( """ -@p0='1' (Nullable = false) (DbType = Object) -@p1='1' (Nullable = false) (DbType = Object) +@p0='2' (Nullable = false) (DbType = Object) +@p1='2' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,OwnedCollectionBranch,1,Enum}', @p0), "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch,Enum}', @p1) @@ -512,7 +512,7 @@ public override async Task Edit_single_property_bool() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -534,7 +534,7 @@ public override async Task Edit_single_property_byte() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -555,7 +555,7 @@ public override async Task Edit_single_property_char() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -577,7 +577,7 @@ public override async Task Edit_single_property_datetime() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -599,7 +599,7 @@ public override async Task Edit_single_property_datetimeoffset() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -621,7 +621,7 @@ public override async Task Edit_single_property_decimal() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -643,7 +643,7 @@ public override async Task Edit_single_property_double() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -665,7 +665,7 @@ public override async Task Edit_single_property_guid() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -687,7 +687,7 @@ public override async Task Edit_single_property_int16() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -709,7 +709,7 @@ public override async Task Edit_single_property_int32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -731,7 +731,7 @@ public override async Task Edit_single_property_int64() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -753,7 +753,7 @@ public override async Task Edit_single_property_signed_byte() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -775,7 +775,7 @@ public override async Task Edit_single_property_single() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -797,7 +797,7 @@ public override async Task Edit_single_property_timespan() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -819,7 +819,7 @@ public override async Task Edit_single_property_uint16() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -841,7 +841,7 @@ public override async Task Edit_single_property_uint32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -863,7 +863,7 @@ public override async Task Edit_single_property_uint64() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -885,7 +885,7 @@ public override async Task Edit_single_property_nullable_int32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -907,7 +907,7 @@ public override async Task Edit_single_property_nullable_int32_set_to_null() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -920,8 +920,8 @@ public override async Task Edit_single_property_enum() AssertSql( """ -@p0='2' (Nullable = false) (DbType = Object) -@p1='2' (Nullable = false) (DbType = Object) +@p0='-3' (Nullable = false) (DbType = Object) +@p1='-3' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnum}', @p0), "Reference" = jsonb_set("Reference", '{TestEnum}', @p1) @@ -929,7 +929,7 @@ public override async Task Edit_single_property_enum() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -942,8 +942,8 @@ public override async Task Edit_single_property_enum_with_int_converter() AssertSql( """ -@p0='2' (Nullable = false) (DbType = Object) -@p1='2' (Nullable = false) (DbType = Object) +@p0='-3' (Nullable = false) (DbType = Object) +@p1='-3' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnumWithIntConverter}', @p0), "Reference" = jsonb_set("Reference", '{TestEnumWithIntConverter}', @p1) @@ -951,7 +951,7 @@ public override async Task Edit_single_property_enum_with_int_converter() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -964,8 +964,8 @@ public override async Task Edit_single_property_nullable_enum() AssertSql( """ -@p0='2' (Nullable = false) (DbType = Object) -@p1='2' (Nullable = false) (DbType = Object) +@p0='-3' (Nullable = false) (DbType = Object) +@p1='-3' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnum}', @p0), "Reference" = jsonb_set("Reference", '{TestEnum}', @p1) @@ -973,7 +973,7 @@ public override async Task Edit_single_property_nullable_enum() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -995,7 +995,7 @@ public override async Task Edit_single_property_nullable_enum_set_to_null() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1008,8 +1008,8 @@ public override async Task Edit_single_property_nullable_enum_with_int_converter AssertSql( """ -@p0='0' (Nullable = false) (DbType = Object) -@p1='2' (Nullable = false) (DbType = Object) +@p0='-1' (Nullable = false) (DbType = Object) +@p1='-3' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverter}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverter}', @p1) @@ -1017,7 +1017,7 @@ public override async Task Edit_single_property_nullable_enum_with_int_converter """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1039,7 +1039,7 @@ public override async Task Edit_single_property_nullable_enum_with_int_converter """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1061,7 +1061,7 @@ public override async Task Edit_single_property_nullable_enum_with_converter_tha """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1083,7 +1083,7 @@ public override async Task Edit_single_property_nullable_enum_with_converter_tha """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1096,8 +1096,8 @@ public override async Task Edit_two_properties_on_same_entity_updates_the_entire AssertSql( """ -@p0='{"TestBoolean":false,"TestBooleanCollection":[true,false],"TestByte":25,"TestByteCollection":null,"TestCharacter":"h","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2323-04-03","TestDateOnlyCollection":["3234-01-23","4331-01-21"],"TestDateTime":"2100-11-11T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2200-11-11T12:34:56-05:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-123450.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInCollection1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.2345,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"00000000-0000-0000-0000-000000000000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-12,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Baz","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Two","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":2,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":90,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-18,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.4,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"05:07:08.0000000","TestTimeOnlyCollection":["13:42:23.0000000","07:17:25.0000000"],"TestTimeSpan":"06:05:04.003","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":12,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":12345,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567867,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) -@p1='{"TestBoolean":true,"TestBooleanCollection":[true,false],"TestByte":255,"TestByteCollection":null,"TestCharacter":"a","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2023-10-10","TestDateOnlyCollection":["1234-01-23","4321-01-21"],"TestDateTime":"2000-01-01T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2000-01-01T12:34:56-08:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-1234567890.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInReference1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.23456789,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"12345678-1234-4321-7777-987654321000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-1234,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Foo","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Three","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":1,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":78,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-128,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.234,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"11:12:13.0000000","TestTimeOnlyCollection":["11:42:23.0000000","07:17:27.0000000"],"TestTimeSpan":"10:09:08.007","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":1234,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":1234565789,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567890123456789,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) +@p0='{"TestBoolean":false,"TestBooleanCollection":[true,false],"TestByte":25,"TestByteArray":"","TestByteCollection":null,"TestCharacter":"h","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2323-04-03","TestDateOnlyCollection":["3234-01-23","4331-01-21"],"TestDateTime":"2100-11-11T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2200-11-11T12:34:56-05:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-123450.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInCollection1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.2345,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":-1,"TestEnumCollection":[-1,-3,-7],"TestEnumWithIntConverter":2,"TestEnumWithIntConverterCollection":[-1,-3,-7],"TestGuid":"00000000-0000-0000-0000-000000000000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-12,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Baz","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":-1,"TestNullableEnumCollection":[-1,null,-3,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Two","TestNullableEnumWithIntConverter":-3,"TestNullableEnumWithIntConverterCollection":[-1,null,-3,-7],"TestNullableInt32":90,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-18,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.4,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"05:07:08.0000000","TestTimeOnlyCollection":["13:42:23.0000000","07:17:25.0000000"],"TestTimeSpan":"06:05:04.003","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":12,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":12345,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567867,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) +@p1='{"TestBoolean":true,"TestBooleanCollection":[true,false],"TestByte":255,"TestByteArray":"AQID","TestByteCollection":null,"TestCharacter":"a","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2023-10-10","TestDateOnlyCollection":["1234-01-23","4321-01-21"],"TestDateTime":"2000-01-01T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2000-01-01T12:34:56-08:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-1234567890.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInReference1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.23456789,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":-1,"TestEnumCollection":[-1,-3,-7],"TestEnumWithIntConverter":2,"TestEnumWithIntConverterCollection":[-1,-3,-7],"TestGuid":"12345678-1234-4321-7777-987654321000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-1234,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Foo","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":-1,"TestNullableEnumCollection":[-1,null,-3,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Three","TestNullableEnumWithIntConverter":2,"TestNullableEnumWithIntConverterCollection":[-1,null,-3,-7],"TestNullableInt32":78,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-128,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.234,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"11:12:13.0000000","TestTimeOnlyCollection":["11:42:23.0000000","07:17:27.0000000"],"TestTimeSpan":"10:09:08.007","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":1234,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":1234565789,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567890123456789,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0}', @p0), "Reference" = @p1 @@ -1105,7 +1105,7 @@ public override async Task Edit_two_properties_on_same_entity_updates_the_entire """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1118,7 +1118,7 @@ public override async Task Edit_a_scalar_property_and_reference_navigation_on_th AssertSql( """ -@p0='{"Date":"2100-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":523.532,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit"}}' (Nullable = false) (DbType = Object) +@p0='{"Date":"2100-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":523.532,"Id":88,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit"}}' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch}', @p0) @@ -1138,7 +1138,7 @@ public override async Task Edit_a_scalar_property_and_collection_navigation_on_t AssertSql( """ -@p0='{"Date":"2100-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":523.532,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"edit"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_r_r"}}' (Nullable = false) (DbType = Object) +@p0='{"Date":"2100-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":523.532,"Id":88,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"edit"}],"OwnedReferenceLeaf":{"SomethingSomething":"e1_r_r_r"}}' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch}', @p0) @@ -1158,7 +1158,7 @@ public override async Task Edit_a_scalar_property_and_another_property_behind_re AssertSql( """ -@p0='{"Date":"2100-01-01T00:00:00","Enum":0,"Enums":[0,-1,1],"Fraction":523.532,"NullableEnum":null,"NullableEnums":[null,-1,1],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit"}}' (Nullable = false) (DbType = Object) +@p0='{"Date":"2100-01-01T00:00:00","Enum":-1,"Enums":[-1,-1,2],"Fraction":523.532,"Id":88,"NullableEnum":null,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"edit"}}' (Nullable = false) (DbType = Object) @p1='1' UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch}', @p0) @@ -1336,7 +1336,7 @@ public override async Task Edit_single_property_collection_of_bool() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1358,7 +1358,7 @@ public override async Task Edit_single_property_collection_of_byte() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1388,7 +1388,7 @@ public override async Task Edit_single_property_collection_of_datetime() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1410,7 +1410,7 @@ public override async Task Edit_single_property_collection_of_datetimeoffset() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1432,7 +1432,7 @@ public override async Task Edit_single_property_collection_of_decimal() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1454,7 +1454,7 @@ public override async Task Edit_single_property_collection_of_double() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1476,7 +1476,7 @@ public override async Task Edit_single_property_collection_of_guid() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1498,7 +1498,7 @@ public override async Task Edit_single_property_collection_of_int16() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1520,7 +1520,7 @@ public override async Task Edit_single_property_collection_of_int32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1542,7 +1542,7 @@ public override async Task Edit_single_property_collection_of_int64() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1564,7 +1564,7 @@ public override async Task Edit_single_property_collection_of_signed_byte() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1586,7 +1586,7 @@ public override async Task Edit_single_property_collection_of_single() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1608,7 +1608,7 @@ public override async Task Edit_single_property_collection_of_timespan() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1630,7 +1630,7 @@ public override async Task Edit_single_property_collection_of_dateonly() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1652,7 +1652,7 @@ public override async Task Edit_single_property_collection_of_timeonly() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1674,7 +1674,7 @@ public override async Task Edit_single_property_collection_of_uint16() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1696,7 +1696,7 @@ public override async Task Edit_single_property_collection_of_uint32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1718,7 +1718,7 @@ public override async Task Edit_single_property_collection_of_uint64() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1740,7 +1740,7 @@ public override async Task Edit_single_property_collection_of_nullable_int32() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1762,7 +1762,7 @@ public override async Task Edit_single_property_collection_of_nullable_int32_set """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1775,8 +1775,8 @@ public override async Task Edit_single_property_collection_of_enum() AssertSql( """ -@p0='[2]' (Nullable = false) (DbType = Object) -@p1='[2]' (Nullable = false) (DbType = Object) +@p0='[-3]' (Nullable = false) (DbType = Object) +@p1='[-3]' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnumCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestEnumCollection}', @p1) @@ -1784,7 +1784,7 @@ public override async Task Edit_single_property_collection_of_enum() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1797,8 +1797,8 @@ public override async Task Edit_single_property_collection_of_enum_with_int_conv AssertSql( """ -@p0='[2]' (Nullable = false) (DbType = Object) -@p1='[2]' (Nullable = false) (DbType = Object) +@p0='[-3]' (Nullable = false) (DbType = Object) +@p1='[-3]' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnumWithIntConverterCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestEnumWithIntConverterCollection}', @p1) @@ -1806,7 +1806,7 @@ public override async Task Edit_single_property_collection_of_enum_with_int_conv """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1819,8 +1819,8 @@ public override async Task Edit_single_property_collection_of_nullable_enum() AssertSql( """ -@p0='[2]' (Nullable = false) (DbType = Object) -@p1='[2]' (Nullable = false) (DbType = Object) +@p0='[-3]' (Nullable = false) (DbType = Object) +@p1='[-3]' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestEnumCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestEnumCollection}', @p1) @@ -1828,7 +1828,7 @@ public override async Task Edit_single_property_collection_of_nullable_enum() """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1850,7 +1850,7 @@ public override async Task Edit_single_property_collection_of_nullable_enum_set_ """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1863,8 +1863,8 @@ public override async Task Edit_single_property_collection_of_nullable_enum_with AssertSql( """ -@p0='[0,null,-7,1]' (Nullable = false) (DbType = Object) -@p1='[0,2,-7,1]' (Nullable = false) (DbType = Object) +@p0='[-1,null,-7,2]' (Nullable = false) (DbType = Object) +@p1='[-1,-3,-7,2]' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverterCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverterCollection}', @p1) @@ -1872,7 +1872,7 @@ public override async Task Edit_single_property_collection_of_nullable_enum_with """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1894,29 +1894,7 @@ public override async Task Edit_single_property_collection_of_nullable_enum_with """, // """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" -FROM "JsonEntitiesAllTypes" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls() - { - await base.Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls(); - - AssertSql( - """ -@p0='[2]' (Nullable = false) (DbType = Object) -@p1='[0]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNullsCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNullsCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1929,16 +1907,7 @@ public override async Task Edit_single_property_collection_of_nullable_enum_with AssertSql( """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNullsCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNullsCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" +SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestInt32Collection", j."TestMaxLengthStringCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j WHERE j."Id" = 1 LIMIT 2 @@ -1982,24 +1951,27 @@ public override Task Edit_single_property_relational_collection_of_enum() public override Task Edit_single_property_relational_collection_of_int16() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_int16()); + public override Task Edit_single_property_relational_collection_of_guid() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_int16()); + public override Task Edit_single_property_relational_collection_of_nullable_enum() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_enum()); public override Task Edit_single_property_relational_collection_of_nullable_enum_set_to_null() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_enum_set_to_null()); + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_enum_set_to_null()); public override Task Edit_single_property_relational_collection_of_nullable_enum_with_int_converter() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_enum_with_int_converter()); public override Task Edit_single_property_relational_collection_of_nullable_enum_with_int_converter_set_to_null() - => Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Edit_single_property_relational_collection_of_nullable_enum_with_int_converter_set_to_null()); public override Task Edit_single_property_relational_collection_of_nullable_int32() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_int32()); public override Task Edit_single_property_relational_collection_of_nullable_int32_set_to_null() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_int32_set_to_null()); + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_nullable_int32_set_to_null()); public override Task Edit_single_property_relational_collection_of_uint16() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_uint16()); @@ -2007,8 +1979,53 @@ public override Task Edit_single_property_relational_collection_of_uint16() public override Task Edit_single_property_relational_collection_of_uint64() => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_uint64()); + public override Task Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls() + => Assert.ThrowsAsync( + () => base.Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls()); + + public override Task Edit_single_property_relational_collection_of_nullable_enum_with_converter_that_handles_nulls() + => Assert.ThrowsAsync( + () => base.Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls()); + #endregion + #region Skipped tests because of nested collections outside of JSON (nested arrays not supported in PG) + + public override Task Edit_single_property_collection_of_collection_of_bool() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_char() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_double() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_int16() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_int32() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_int64() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_single() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_nullable_int32() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_nullable_int32_set_to_null() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_nullable_enum_set_to_null() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + public override Task Edit_single_property_collection_of_collection_of_nullable_enum_with_int_converter() + => Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_collection_of_bool()); + + #endregion Skipped tests because of nested collections outside of JSON (nested arrays not supported in PG) + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); @@ -2054,6 +2071,77 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Ignore(j => j.TestDoubleCollection); b.Ignore(j => j.TestInt16Collection); }); + + // These use collection types which are unsupported for arrays at the Npgsql level - we currently only support List/array. + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestInt64Collection); + b.Ignore(j => j.TestGuidCollection); + }); + + // Ignore nested collections - these aren't supported on PostgreSQL (no arrays of arrays). + // TODO: Remove these after syncing to 9.0.0-rc.1, and extending from the relational test base and fixture + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); + + modelBuilder.Entity().OwnsOne( + x => x.Reference, b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); + + modelBuilder.Entity().OwnsMany( + x => x.Collection, b => + { + b.Ignore(j => j.TestDefaultStringCollectionCollection); + b.Ignore(j => j.TestMaxLengthStringCollectionCollection); + + b.Ignore(j => j.TestInt16CollectionCollection); + b.Ignore(j => j.TestInt32CollectionCollection); + b.Ignore(j => j.TestInt64CollectionCollection); + b.Ignore(j => j.TestDoubleCollectionCollection); + b.Ignore(j => j.TestSingleCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestBooleanCollectionCollection); + b.Ignore(j => j.TestCharacterCollectionCollection); + + b.Ignore(j => j.TestNullableInt32CollectionCollection); + b.Ignore(j => j.TestNullableEnumCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollectionCollection); + b.Ignore(j => j.TestNullableEnumWithConverterThatHandlesNullsCollection); + }); } } } diff --git a/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs index 0c4b0fc3c..b44952233 100644 --- a/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs @@ -69,7 +69,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity() .Property(p => p.Id).HasDefaultValueSql("uuid_generate_v4()"); - modelBuilder.Entity().HasIndex(p => new { p.Name, p.Price }).IsUnique().HasFilter(@"""Name"" IS NOT NULL"); + modelBuilder.Entity().HasIndex(p => new { p.Name, p.Price }).IsUnique().HasFilter(""" + "Name" IS NOT NULL + """); modelBuilder.Entity().Property(r => r.Concurrency).HasColumnType("timestamp without time zone"); } diff --git a/test/EFCore.PG.FunctionalTests/ValueConvertersEndToEndNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ValueConvertersEndToEndNpgsqlTest.cs index 51734f48a..56ea743cf 100644 --- a/test/EFCore.PG.FunctionalTests/ValueConvertersEndToEndNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/ValueConvertersEndToEndNpgsqlTest.cs @@ -2,16 +2,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class ValueConvertersEndToEndNpgsqlTest - : ValueConvertersEndToEndTestBase +public class ValueConvertersEndToEndNpgsqlTest(ValueConvertersEndToEndNpgsqlTest.ValueConvertersEndToEndNpgsqlFixture fixture) + : ValueConvertersEndToEndTestBase(fixture) { - public ValueConvertersEndToEndNpgsqlTest(ValueConvertersEndToEndNpgsqlFixture fixture) - : base(fixture) - { - } - [ConditionalTheory(Skip = "DateTime and DateTimeOffset, https://github.com/dotnet/efcore/issues/26068")] - public override void Can_insert_and_read_back_with_conversions(int[] valueOrder) + public override Task Can_insert_and_read_back_with_conversions(int[] valueOrder) => base.Can_insert_and_read_back_with_conversions(valueOrder); [ConditionalTheory] @@ -163,7 +158,7 @@ public async Task Can_insert_and_read_back_with_value_converted_array() { await using var ctx = CreateContext(); - var entity = new ValueConvertedArrayEntity { Values = new IntWrapper[] { new(8), new(9) } }; + var entity = new ValueConvertedArrayEntity { Values = [new(8), new(9)] }; ctx.Add(entity); await ctx.SaveChangesAsync(); @@ -211,14 +206,9 @@ public class ValueConvertedArrayEntity public IntWrapper[] Values { get; set; } = null!; } - public class IntWrapper : IEquatable + public class IntWrapper(int value) : IEquatable { - public int Value { get; } - - public IntWrapper(int value) - { - Value = value; - } + public int Value { get; } = value; public bool Equals(IntWrapper? other) => other is not null && Value == other.Value; diff --git a/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs index d82eec61b..14ce4b0b4 100644 --- a/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs @@ -2,13 +2,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class WithConstructorsNpgsqlTest : WithConstructorsTestBase +public class WithConstructorsNpgsqlTest(WithConstructorsNpgsqlTest.WithConstructorsNpgsqlFixture fixture) + : WithConstructorsTestBase(fixture) { - public WithConstructorsNpgsqlTest(WithConstructorsNpgsqlFixture fixture) - : base(fixture) - { - } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); @@ -21,7 +17,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity().HasNoKey().ToSqlQuery(@"SELECT * FROM ""Blog"""); + modelBuilder.Entity().HasNoKey().ToSqlQuery(""" + SELECT * FROM "Blog" + """); } } } diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/LegacyNpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/LegacyNpgsqlNodaTimeTypeMappingTest.cs index 4c25a7e8f..e6e49a8be 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/LegacyNpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/LegacyNpgsqlNodaTimeTypeMappingTest.cs @@ -63,7 +63,7 @@ public void GenerateSqlLiteral_returns_instant_range_in_legacy_mode() new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty()), + []), new RelationalTypeMappingSourceDependencies( new IRelationalTypeMappingSourcePlugin[] { @@ -98,9 +98,7 @@ public void Dispose() } [CollectionDefinition("LegacyNodaTimeTest", DisableParallelization = true)] - public class EventSourceTestCollection - { - } + public class EventSourceTestCollection; } #endif diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 6886d312c..ec9815e04 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -357,7 +357,8 @@ 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())); + == new ZonedDateTime( + new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); AssertSql( """ @@ -1208,16 +1209,16 @@ public async Task Interval_RangeAgg(bool async) : query.Single(); var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); - Assert.Equal(new Interval[] { new(start, start + Duration.FromDays(5)) }, union); + Assert.Equal([new(start, start + Duration.FromDays(5))], union); AssertSql( """ -SELECT range_agg(t."Interval") +SELECT range_agg(n0."Interval") FROM ( SELECT n."Interval", TRUE AS "Key" FROM "NodaTimeTypes" AS n -) AS t -GROUP BY t."Key" +) AS n0 +GROUP BY n0."Key" LIMIT 2 """); } @@ -1242,12 +1243,12 @@ public async Task Interval_Intersect_aggregate(bool async) AssertSql( """ -SELECT range_intersect_agg(t."Interval") +SELECT range_intersect_agg(n0."Interval") FROM ( SELECT n."Interval", TRUE AS "Key" FROM "NodaTimeTypes" AS n -) AS t -GROUP BY t."Key" +) AS n0 +GROUP BY n0."Key" LIMIT 2 """); } @@ -1300,7 +1301,22 @@ await AssertQuery( """ 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") - INTERVAL 'P1D' = DATE '2018-04-24' +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 """); } @@ -1401,16 +1417,16 @@ public async Task DateInterval_RangeAgg(bool async) ? await query.SingleAsync() : query.Single(); - Assert.Equal(new DateInterval[] { new(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24)) }, union); + Assert.Equal([new(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24))], union); AssertSql( """ -SELECT range_agg(t."DateInterval") +SELECT range_agg(n0."DateInterval") FROM ( SELECT n."DateInterval", TRUE AS "Key" FROM "NodaTimeTypes" AS n -) AS t -GROUP BY t."Key" +) AS n0 +GROUP BY n0."Key" LIMIT 2 """); } @@ -1434,12 +1450,12 @@ public async Task DateInterval_Intersect_aggregate(bool async) AssertSql( """ -SELECT range_intersect_agg(t."DateInterval") +SELECT range_intersect_agg(n0."DateInterval") FROM ( SELECT n."DateInterval", TRUE AS "Key" FROM "NodaTimeTypes" AS n -) AS t -GROUP BY t."Key" +) AS n0 +GROUP BY n0."Key" LIMIT 2 """); } @@ -1802,12 +1818,12 @@ await AssertQuery( 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( @@ -1839,13 +1855,8 @@ private NodaTimeContext CreateContext() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class NodaTimeContext : PoolableDbContext + public class NodaTimeContext(DbContextOptions options) : PoolableDbContext(options) { - public NodaTimeContext(DbContextOptions options) - : base(options) - { - } - // ReSharper disable once MemberHidesStaticFromOuterClass // ReSharper disable once UnusedAutoPropertyAccessor.Global public DbSet NodaTimeTypes { get; set; } @@ -1892,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; @@ -1968,12 +1974,7 @@ public IReadOnlyDictionary EntityAsserters private class NodaTimeData : ISetSource { - private IReadOnlyList NodaTimeTypes { get; } - - public NodaTimeData() - { - NodaTimeTypes = CreateNodaTimeTypes(); - } + private IReadOnlyList NodaTimeTypes { get; } = CreateNodaTimeTypes(); public IQueryable Set() where TEntity : class diff --git a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs b/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs index f0323eac3..ec9cff986 100644 --- a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs +++ b/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs @@ -404,9 +404,9 @@ private NpgsqlAnnotationCodeGenerator CreateGenerator() new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty() + [] ), - new RelationalTypeMappingSourceDependencies(Array.Empty()), + new RelationalTypeMappingSourceDependencies([]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), new NpgsqlSingletonOptions()))); } diff --git a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj index f828e9b41..7f23750d4 100644 --- a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj +++ b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj @@ -3,6 +3,10 @@ Npgsql.EntityFrameworkCore.PostgreSQL.Tests Npgsql.EntityFrameworkCore.PostgreSQL + + $(NoWarn);NU1903 + false + critical @@ -18,6 +22,10 @@ + + + + diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs index f16f06749..b4efac2bd 100644 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs +++ b/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs @@ -11,7 +11,7 @@ public void CockroachDbInterleaveInParent() modelBuilder.Entity() .ToTable("customers", "my_schema") - .UseCockroachDbInterleaveInParent(typeof(Customer), new List { "col_a", "col_b" }); + .UseCockroachDbInterleaveInParent(typeof(Customer), ["col_a", "col_b"]); var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); var interleaveInParent = entityType.GetCockroachDbInterleaveInParent(); diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs index 9e8288852..91aa83076 100644 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs +++ b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -75,31 +74,13 @@ public void Can_access_property() a => a.Name.StartsWith(NpgsqlAnnotationNames.Prefix, StringComparison.Ordinal))); } - [ConditionalFact] - public void Throws_setting_sequence_generation_for_invalid_type() - { - var propertyBuilder = CreateBuilder() - .Entity(typeof(Splot)) - .Property(typeof(string), "Name"); - - Assert.Equal( - NpgsqlStrings.SequenceBadType("Name", nameof(Splot), "string"), - Assert.Throws( - () => propertyBuilder.HasValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo)).Message); - - Assert.Equal( - NpgsqlStrings.SequenceBadType("Name", nameof(Splot), "string"), - Assert.Throws( - () => new PropertyBuilder((IMutableProperty)propertyBuilder.Metadata).UseHiLo()).Message); - } - [ConditionalFact] public void Can_access_index() { var modelBuilder = CreateBuilder(); var entityTypeBuilder = modelBuilder.Entity(typeof(Splot)); var idProperty = entityTypeBuilder.Property(typeof(int), "Id").Metadata; - var indexBuilder = entityTypeBuilder.HasIndex(new[] { idProperty }); + var indexBuilder = entityTypeBuilder.HasIndex([idProperty]); Assert.NotNull(indexBuilder.HasMethod("gin")); Assert.Equal("gin", indexBuilder.Metadata.GetMethod()); @@ -121,7 +102,7 @@ public void Can_access_relationship() var modelBuilder = CreateBuilder(); var entityTypeBuilder = modelBuilder.Entity(typeof(Splot)); var idProperty = entityTypeBuilder.Property(typeof(int), "Id").Metadata; - var key = entityTypeBuilder.HasKey(new[] { idProperty }).Metadata; + var key = entityTypeBuilder.HasKey([idProperty]).Metadata; var relationshipBuilder = entityTypeBuilder.HasRelationship(entityTypeBuilder.Metadata, key); Assert.NotNull(relationshipBuilder.HasConstraintName("Splew")); @@ -138,7 +119,5 @@ public void Can_access_relationship() a => a.Name.StartsWith(RelationalAnnotationNames.Prefix, StringComparison.Ordinal))); } - private class Splot - { - } + private class Splot; } diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs index 38cee7e59..046271648 100644 --- a/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs +++ b/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs @@ -128,7 +128,6 @@ public void GetBeginIfExistsScript_works() Assert.Equal( """ - DO $EF$ BEGIN IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = 'Migration1') THEN @@ -157,13 +156,8 @@ private static IHistoryRepository CreateHistoryRepository(string schema = null) .Options) .GetService(); - private class TestDbContext : DbContext + private class TestDbContext(DbContextOptions options) : DbContext(options) { - public TestDbContext(DbContextOptions options) - : base(options) - { - } - public DbSet Blogs { get; set; } [DbFunction("TableFunction")] diff --git a/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs b/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs index db82de596..e8e29ad87 100644 --- a/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs @@ -141,13 +141,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } } - private class ProviderOnModelContext : ProviderContext + private class ProviderOnModelContext(DbContextOptions options) : ProviderContext(options) { - public ProviderOnModelContext(DbContextOptions options) - : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) => IsNpgsqlSet = Database.IsNpgsql(); } @@ -162,13 +157,8 @@ public ProviderConstructorContext(DbContextOptions options) } } - private class ProviderUseInOnConfiguringContext : ProviderContext + private class ProviderUseInOnConfiguringContext(DbContextOptions options) : ProviderContext(options) { - public ProviderUseInOnConfiguringContext(DbContextOptions options) - : base(options) - { - } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => IsNpgsqlSet = Database.IsNpgsql(); } diff --git a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs b/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs index 2b4cfb7aa..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( @@ -116,11 +116,5 @@ public void Service_collection_extension_method_can_configure_sqlserver_options( } } - private class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - } + private class ApplicationDbContext(DbContextOptions options) : DbContext(options); } diff --git a/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs b/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs index 5febe0b86..029c02ecb 100644 --- a/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs @@ -1,13 +1,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class NpgsqlNetTopologySuiteApiConsistencyTest : ApiConsistencyTestBase< - NpgsqlNetTopologySuiteApiConsistencyTest.NpgsqlNetTopologySuiteApiConsistencyFixture> +public class NpgsqlNetTopologySuiteApiConsistencyTest( + NpgsqlNetTopologySuiteApiConsistencyTest.NpgsqlNetTopologySuiteApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) { - public NpgsqlNetTopologySuiteApiConsistencyTest(NpgsqlNetTopologySuiteApiConsistencyFixture fixture) - : base(fixture) - { - } - protected override void AddServices(ServiceCollection serviceCollection) => serviceCollection.AddEntityFrameworkNpgsqlNetTopologySuite(); @@ -16,9 +12,7 @@ protected override Assembly TargetAssembly public class NpgsqlNetTopologySuiteApiConsistencyFixture : ApiConsistencyFixtureBase { - public override HashSet FluentApiTypes { get; } = new() - { - typeof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions), typeof(NpgsqlNetTopologySuiteServiceCollectionExtensions) - }; + public override HashSet FluentApiTypes { get; } = + [typeof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions), typeof(NpgsqlNetTopologySuiteServiceCollectionExtensions)]; } } diff --git a/test/EFCore.PG.Tests/NpgsqlNodaTimeApiConsistencyTest.cs b/test/EFCore.PG.Tests/NpgsqlNodaTimeApiConsistencyTest.cs index 9a76028f4..d1e6798a4 100644 --- a/test/EFCore.PG.Tests/NpgsqlNodaTimeApiConsistencyTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlNodaTimeApiConsistencyTest.cs @@ -1,12 +1,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; -public class NpgsqlNodaTimeApiConsistencyTest : ApiConsistencyTestBase +public class NpgsqlNodaTimeApiConsistencyTest(NpgsqlNodaTimeApiConsistencyTest.NpgsqlNodaTimeApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) { - public NpgsqlNodaTimeApiConsistencyTest(NpgsqlNodaTimeApiConsistencyFixture fixture) - : base(fixture) - { - } - protected override void AddServices(ServiceCollection serviceCollection) => serviceCollection.AddEntityFrameworkNpgsqlNodaTime(); @@ -15,9 +11,7 @@ protected override Assembly TargetAssembly public class NpgsqlNodaTimeApiConsistencyFixture : ApiConsistencyFixtureBase { - public override HashSet FluentApiTypes { get; } = new() - { - typeof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions), typeof(NpgsqlNodaTimeServiceCollectionExtensions) - }; + public override HashSet FluentApiTypes { get; } = + [typeof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions), typeof(NpgsqlNodaTimeServiceCollectionExtensions)]; } } diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs index 8d152daed..3f7d9d3ed 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; @@ -11,10 +12,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; +#nullable enable + public class NpgsqlRelationalConnectionTest { [Fact] - public void Creates_Npgsql_Server_connection_string() + public void Creates_NpgsqlConnection() { using var connection = CreateConnection(); @@ -30,13 +33,14 @@ public void Uses_DbDataSource_from_DbContextOptions() serviceCollection .AddNpgsqlDataSource("Host=FakeHost") + // ReSharper disable once AccessToDisposedClosure .AddDbContext(o => o.UseNpgsql(dataSource)); using var serviceProvider = serviceCollection.BuildServiceProvider(); using var scope1 = serviceProvider.CreateScope(); var context1 = scope1.ServiceProvider.GetRequiredService(); - var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService()!; + var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService(); Assert.Same(dataSource, relationalConnection1.DbDataSource); var connection1 = context1.GetService().Database.GetDbConnection(); @@ -44,7 +48,7 @@ public void Uses_DbDataSource_from_DbContextOptions() using var scope2 = serviceProvider.CreateScope(); var context2 = scope2.ServiceProvider.GetRequiredService(); - var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService()!; + var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService(); Assert.Same(dataSource, relationalConnection2.DbDataSource); var connection2 = context2.GetService().Database.GetDbConnection(); @@ -66,7 +70,7 @@ public void Uses_DbDataSource_from_application_service_provider() using var scope1 = serviceProvider.CreateScope(); var context1 = scope1.ServiceProvider.GetRequiredService(); - var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService()!; + var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService(); Assert.Same(dataSource, relationalConnection1.DbDataSource); var connection1 = context1.GetService().Database.GetDbConnection(); @@ -74,13 +78,211 @@ public void Uses_DbDataSource_from_application_service_provider() using var scope2 = serviceProvider.CreateScope(); var context2 = scope2.ServiceProvider.GetRequiredService(); - var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService()!; + var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService(); Assert.Same(dataSource, relationalConnection2.DbDataSource); var connection2 = context2.GetService().Database.GetDbConnection(); Assert.Equal("Host=FakeHost", connection2.ConnectionString); } + [Fact] // #3060 + public void DbDataSource_from_application_service_provider_does_not_used_if_connection_string_is_specified() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddNpgsqlDataSource("Host=FakeHost1") + .AddDbContext(o => o.UseNpgsql("Host=FakeHost2")); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Null(relationalConnection1.DbDataSource); + + var connection1 = context1.GetService().Database.GetDbConnection(); + Assert.Equal("Host=FakeHost2", connection1.ConnectionString); + } + + [Fact] + public void Data_source_config_with_same_connection_string() + { + // The connection string is the same, so the same data source gets resolved. + // This works well as long as ConfigureDataSource() has the same lambda. + var context1 = new ConfigurableContext( + "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext( + "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1")); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Data_source_config_with_different_connection_strings() + { + // When different connection strings are used, different data sources are created internally. + var context1 = new ConfigurableContext( + "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext( + "Host=FakeHost2", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App2")); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost2;Application Name=App2", connection2.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Data_source_config_with_same_connection_string_and_different_lambda() + { + // Bad case: same connection string but with a different data source config lambda. + // Same data source gets reused, and so the differing data source config gets ignored. + var context1 = new ConfigurableContext( + "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext( + "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App2")); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + // Note the incorrect Application Name below, because the 1st data source was resolved based on the connection string only + Assert.Equal("Host=FakeHost1;Application Name=App1", connection2.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Plugin_config_with_same_connection_string() + { + // The connection string and plugin config are the same, so the same data source gets resolved. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite()); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite()); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Plugin_config_with_different_connection_strings() + { + // When different connection strings are used, different data sources are created internally. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite()); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost2", no => no.UseNetTopologySuite()); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost2", connection2.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Plugin_config_with_different_connection_strings_and_different_plugins() + { + // Since the plugin configuration is a singleton option, a different service provider gets resolved and we have different data + // sources. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite()); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNodaTime()); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Enum_config_with_same_connection_string() + { + // The connection string and plugin config are the same, so the same data source gets resolved. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood")); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Enum_config_with_different_connection_strings() + { + // When different connection strings are used, different data sources are created internally. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost2", no => no.MapEnum("mood")); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost2", connection2.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Enum_config_with_different_connection_strings_and_different_enums() + { + // Since the enum configuration is a singleton option, a different service provider gets resolved, and we have different data + // sources. + var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood")); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", _ => { /* no enums */}); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource); + } + + [Fact] + public void Data_source_and_data_source_config_are_incompatible() + { + using var dataSource = NpgsqlDataSource.Create("Host=FakeHost"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(dataSource, no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "foo")); + + var context1 = new FakeDbContext(optionsBuilder.Options); + var exception = Assert.Throws(() => context1.GetService()); + Assert.Equal(NpgsqlStrings.DataSourceAndConfigNotSupported, exception.Message); + } + + [Fact] + public void Multiple_connection_strings_without_data_source_features() + { + var context1 = new ConfigurableContext("Host=FakeHost1"); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Null(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1"); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Null(connection2.DbDataSource); + + var context3 = new ConfigurableContext("Host=FakeHost2"); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.Null(connection3.DbDataSource); + } + [Fact] public void Can_create_master_connection_with_connection_string() { @@ -148,7 +350,7 @@ public void CurrentAmbientTransaction_returns_transaction_with_enlist_enabled(st } [ConditionalFact] - public void CloneWith_with_connection_and_connection_string() + public async Task CloneWith_with_connection_and_connection_string() { var services = NpgsqlTestHelpers.Instance.CreateContextServices( new DbContextOptionsBuilder() @@ -157,12 +359,12 @@ public void CloneWith_with_connection_and_connection_string() var relationalConnection = (NpgsqlRelationalConnection)services.GetRequiredService(); - var clone = relationalConnection.CloneWith("Host=localhost;Database=DummyDatabase;Application Name=foo"); + var clone = await relationalConnection.CloneWith("Host=localhost;Database=DummyDatabase;Application Name=foo", async: true); Assert.Equal("Host=localhost;Database=DummyDatabase;Application Name=foo", clone.ConnectionString); } - public static NpgsqlRelationalConnection CreateConnection(DbContextOptions options = null, DbDataSource dataSource = null) + public static NpgsqlRelationalConnection CreateConnection(DbContextOptions? options = null, DbDataSource? dataSource = null) { options ??= new DbContextOptionsBuilder() .UseNpgsql(@"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password") @@ -173,8 +375,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( @@ -191,7 +392,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio new DiagnosticListener("FakeDiagnosticListener"), new NpgsqlLoggingDefinitions(), new NullDbContextLogger(), - CreateOptions()), + dbContextOptions), new NamedConnectionStringResolver(options), new RelationalTransactionFactory( new RelationalTransactionFactoryDependencies( @@ -206,13 +407,13 @@ 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"; - private static IDbContextOptions CreateOptions( - RelationalOptionsExtension optionsExtension = null) + private static IDbContextOptions CreateOptions(RelationalOptionsExtension? optionsExtension = null) { var optionsBuilder = new DbContextOptionsBuilder(); @@ -235,4 +436,18 @@ public FakeDbContext(DbContextOptions options) { } } + + private class ConfigurableContext(string connectionString, Action? npgsqlOptionsAction = null) : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql(connectionString, npgsqlOptionsAction); + } + + private enum Mood + { + // ReSharper disable once UnusedMember.Local + Happy, + // ReSharper disable once UnusedMember.Local + Sad + } } diff --git a/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs b/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs index c5ce7a25a..9b6b3ae42 100644 --- a/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration; using Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL; @@ -20,8 +21,8 @@ public void Returns_built_in_generators_for_types_setup_for_value_generation() AssertGenerator("NullableShort"); AssertGenerator("NullableByte"); AssertGenerator("Decimal"); - AssertGenerator("String"); - AssertGenerator("Guid"); + AssertGenerator("String"); + AssertGenerator("Guid"); AssertGenerator("Binary"); } @@ -47,10 +48,13 @@ private void AssertGenerator(string propertyName, bool setSequences = var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - Assert.IsType(selector.Select(entityType.FindProperty(propertyName), entityType)); + var property = entityType.FindProperty(propertyName)!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType(generator); } - [Fact] + [ConditionalFact] public void Returns_temp_guid_generator_when_default_sql_set() { var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); @@ -65,7 +69,9 @@ public void Returns_temp_guid_generator_when_default_sql_set() var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - Assert.IsType(selector.Select(entityType.FindProperty("Guid"), entityType)); + var property = entityType.FindProperty("Guid")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + Assert.IsType(generator); } [ConditionalFact] @@ -83,7 +89,9 @@ public void Returns_temp_string_generator_when_default_sql_set() var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - var generator = selector.Select(entityType.FindProperty("String"), entityType); + var property = entityType.FindProperty("String")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + Assert.IsType(generator); Assert.True(generator.GeneratesTemporaryValues); } @@ -103,7 +111,9 @@ public void Returns_temp_binary_generator_when_default_sql_set() var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - var generator = selector.Select(entityType.FindProperty("Binary"), entityType); + var property = entityType.FindProperty("Binary")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + Assert.IsType(generator); Assert.True(generator.GeneratesTemporaryValues); } @@ -118,8 +128,8 @@ public void Returns_sequence_value_generators_when_configured_for_model() AssertGenerator>("NullableInt", setSequences: true); AssertGenerator>("NullableLong", setSequences: true); AssertGenerator>("NullableShort", setSequences: true); - AssertGenerator("String", setSequences: true); - AssertGenerator("Guid", setSequences: true); + AssertGenerator("String", setSequences: true); + AssertGenerator("Guid", setSequences: true); AssertGenerator("Binary", setSequences: true); } @@ -159,7 +169,10 @@ public void Returns_generator_configured_on_model_when_property_is_identity() var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - Assert.IsType>(selector.Select(entityType.FindProperty("Id"), entityType)); + var property = entityType.FindProperty("Id")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType>(generator); } private class AnEntity @@ -198,4 +211,19 @@ public override int Next(EntityEntry entry) public override bool GeneratesTemporaryValues => false; } + + [Fact] + public void NpgsqlUuid7ValueGenerator_creates_uuidv7() + { + var dtoNow = DateTimeOffset.UtcNow; + var net9Internal = Guid.CreateVersion7(dtoNow); + var custom = NpgsqlSequentialGuidValueGenerator.BorrowedFromNet9.CreateVersion7(dtoNow); + var bytenet9 = net9Internal.ToByteArray().AsSpan(0, 6); + var bytecustom = custom.ToByteArray().AsSpan(0, 6); + Assert.Equal(bytenet9, bytecustom); + Assert.Equal(7, net9Internal.Version); + Assert.Equal(net9Internal.Version, custom.Version); + Assert.InRange(net9Internal.Variant, 8, 0xB); + Assert.InRange(custom.Variant, 8, 0xB); + } } diff --git a/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs b/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs index 3f68c3341..1405216a8 100644 --- a/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs +++ b/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs @@ -12,7 +12,7 @@ public virtual void Use_provider_method_is_generated_correctly() { var codeGenerator = new NpgsqlCodeGenerator( new ProviderCodeGeneratorDependencies( - Enumerable.Empty())); + [])); var result = codeGenerator.GenerateUseProvider("Server=test;Username=test;Password=test;Database=test", providerOptions: null); @@ -28,7 +28,7 @@ public virtual void Use_provider_method_is_generated_correctly_with_options() { var codeGenerator = new NpgsqlCodeGenerator( new ProviderCodeGeneratorDependencies( - Enumerable.Empty())); + [])); var providerOptions = new MethodCallCodeFragment(_setProviderOptionMethodInfo); @@ -53,7 +53,7 @@ public virtual void Use_provider_method_is_generated_correctly_with_NetTopologyS { var codeGenerator = new NpgsqlCodeGenerator( new ProviderCodeGeneratorDependencies( - new[] { new NpgsqlNetTopologySuiteCodeGeneratorPlugin() })); + [new NpgsqlNetTopologySuiteCodeGeneratorPlugin()])); var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); @@ -76,7 +76,7 @@ public virtual void Use_provider_method_is_generated_correctly_with_NodaTime() { var codeGenerator = new NpgsqlCodeGenerator( new ProviderCodeGeneratorDependencies( - new[] { new NpgsqlNodaTimeCodeGeneratorPlugin() })); + [new NpgsqlNodaTimeCodeGeneratorPlugin()])); var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); @@ -95,7 +95,7 @@ public virtual void Use_provider_method_is_generated_correctly_with_NodaTime() } private static readonly MethodInfo _setProviderOptionMethodInfo - = typeof(NpgsqlCodeGeneratorTest).GetRuntimeMethod(nameof(SetProviderOption), new[] { typeof(DbContextOptionsBuilder) }); + = typeof(NpgsqlCodeGeneratorTest).GetRuntimeMethod(nameof(SetProviderOption), [typeof(DbContextOptionsBuilder)]); public static NpgsqlDbContextOptionsBuilder SetProviderOption(DbContextOptionsBuilder optionsBuilder) => throw new NotSupportedException(); diff --git a/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs index 4c880d6ba..ac536bd4d 100644 --- a/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs @@ -49,9 +49,9 @@ public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty() + [] ), - new RelationalTypeMappingSourceDependencies(Array.Empty()), + new RelationalTypeMappingSourceDependencies([]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), new NpgsqlSingletonOptions()); @@ -76,8 +76,6 @@ public void Dispose() } [CollectionDefinition("LegacyDateTimeTest", DisableParallelization = true)] -public class EventSourceTestCollection -{ -} +public class EventSourceTestCollection; #endif diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs index ebfe3ba45..65c1be941 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs @@ -12,12 +12,12 @@ public void Can_convert_enum_arrays_to_number_arrays() { var converter = EnumArrayToNumberArray.ConvertToProviderExpression.Compile(); - Assert.Equal(new[] { 7 }, converter(new[] { Beatles.John })); - Assert.Equal(new[] { 4 }, converter(new[] { Beatles.Paul })); - Assert.Equal(new[] { 1 }, converter(new[] { Beatles.George })); - Assert.Equal(new[] { -1 }, converter(new[] { Beatles.Ringo })); - Assert.Equal(new[] { 77 }, converter(new[] { (Beatles)77 })); - Assert.Equal(new[] { 0 }, converter(new[] { default(Beatles) })); + Assert.Equal(new[] { 7 }, converter([Beatles.John])); + Assert.Equal(new[] { 4 }, converter([Beatles.Paul])); + Assert.Equal(new[] { 1 }, converter([Beatles.George])); + Assert.Equal(new[] { -1 }, converter([Beatles.Ringo])); + Assert.Equal(new[] { 77 }, converter([(Beatles)77])); + Assert.Equal(new[] { 0 }, converter([default(Beatles)])); Assert.Null(converter(null)); } @@ -40,12 +40,12 @@ public void Can_convert_number_arrays_to_enum_arrays() { var converter = EnumArrayToNumberArray.ConvertFromProviderExpression.Compile(); - Assert.Equal(new[] { Beatles.John }, converter(new[] { 7 })); - Assert.Equal(new[] { Beatles.Paul }, converter(new[] { 4 })); - Assert.Equal(new[] { Beatles.George }, converter(new[] { 1 })); - Assert.Equal(new[] { Beatles.Ringo }, converter(new[] { -1 })); - Assert.Equal(new[] { (Beatles)77 }, converter(new[] { 77 })); - Assert.Equal(new[] { default(Beatles) }, converter(new[] { 0 })); + Assert.Equal([Beatles.John], converter([7])); + Assert.Equal([Beatles.Paul], converter([4])); + Assert.Equal([Beatles.George], converter([1])); + Assert.Equal([Beatles.Ringo], converter([-1])); + Assert.Equal([(Beatles)77], converter([77])); + Assert.Equal([default(Beatles)], converter([0])); Assert.Null(converter(null)); } diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs similarity index 97% rename from test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs rename to test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs index ddf7e078d..1545c1348 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 { @@ -104,7 +105,7 @@ public void GenerateSqlLiteral_returns_tsrange_literal() Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); var value = new NpgsqlRange(new LocalDateTime(2020, 1, 1, 12, 0, 0), new LocalDateTime(2020, 1, 2, 12, 0, 0)); - Assert.Equal(@"'[""2020-01-01T12:00:00"",""2020-01-02T12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00","2020-01-02T12:00:00"]'::tsrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -169,7 +170,7 @@ public void GenerateCodeLiteral_returns_ZonedDateTime_literal() var zonedDateTime = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)) .InZone(DateTimeZone.ForOffset(Offset.FromHours(2)), Resolvers.LenientResolver); Assert.Equal( - @"new NodaTime.ZonedDateTime(NodaTime.Instant.FromUnixTimeTicks(15242130936666660L), NodaTime.TimeZones.TzdbDateTimeZoneSource.Default.ForId(""UTC+02""))", + """new NodaTime.ZonedDateTime(NodaTime.Instant.FromUnixTimeTicks(15242130936666660L), NodaTime.TimeZones.TzdbDateTimeZoneSource.Default.ForId("UTC+02"))""", CodeLiteral(zonedDateTime)); } @@ -368,7 +369,7 @@ public void GenerateSqlLiteral_returns_tstzrange_Instant_literal() 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""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00Z","2020-01-02T12:00:00Z"]'::tstzrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -381,7 +382,7 @@ public void GenerateSqlLiteral_returns_tstzrange_ZonedDateTime_literal() var value = new NpgsqlRange( new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc(), new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc()); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00Z","2020-01-02T12:00:00Z"]'::tstzrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -394,7 +395,7 @@ public void GenerateSqlLiteral_returns_tstzrange_OffsetDateTime_literal() var value = new NpgsqlRange( new LocalDateTime(2020, 1, 1, 12, 0, 0).WithOffset(Offset.Zero), new LocalDateTime(2020, 1, 2, 12, 0, 0).WithOffset(Offset.Zero)); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00Z","2020-01-02T12:00:00Z"]'::tstzrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -687,7 +688,7 @@ public void OffsetTime_json(string timeString, string json) [Fact] public void Duration_is_properly_mapped() => Assert.All( - new[] { GetMapping(typeof(Duration)), GetMapping(typeof(Duration), "interval") }, + [GetMapping(typeof(Duration)), GetMapping(typeof(Duration), "interval")], m => { Assert.Equal("interval", m.StoreType); @@ -697,7 +698,7 @@ public void Duration_is_properly_mapped() [Fact] public void Period_is_properly_mapped() => Assert.All( - new[] { GetMapping(typeof(Period)), GetMapping(typeof(Period), "interval") }, + [GetMapping(typeof(Period)), GetMapping(typeof(Period), "interval")], m => { Assert.Equal("interval", m.StoreType); @@ -838,13 +839,12 @@ public void GenerateCodeLiteral_returns_DateTimezone_literal() new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty()), + []), new RelationalTypeMappingSourceDependencies( - new IRelationalTypeMappingSourcePlugin[] - { - new NpgsqlNodaTimeTypeMappingSourcePlugin( + [ + new NpgsqlNodaTimeTypeMappingSourcePlugin( new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies())) - }), + ]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), new NpgsqlSingletonOptions() ); diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs index cafb9d493..be5062dda 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs @@ -41,6 +41,7 @@ public class NpgsqlTypeMappingSourceTest [InlineData("geometry(POLYGONM)", typeof(Polygon), null, null, null, false)] [InlineData("xid", typeof(uint), null, null, null, false)] [InlineData("xid8", typeof(ulong), null, null, null, false)] + [InlineData("jsonpath", typeof(string), null, null, null, false)] public void By_StoreType(string typeName, Type type, int? size, int? precision, int? scale, bool fixedLength) { var mapping = CreateTypeMappingSource().FindMapping(typeName); @@ -290,6 +291,35 @@ public void Multirange_by_store_type_across_pg_versions() Assert.Same(typeof(List>), mappingDefault.ClrType); } +#nullable enable + [Theory] + [InlineData("integer", "integer", null, null, null, null)] + [InlineData("integer[]", "integer[]", null, null, null, null)] + [InlineData("foo.bar", "bar", "foo", null, null, null)] + [InlineData("foo.bar[]", "foo.bar[]", null, null, null, null)] + [InlineData("\"foo\"", "foo", null, null, null, null)] + [InlineData("\"fo.o\"", "fo.o", null, null, null, null)] + [InlineData("\"foo\".\"bar\"", "bar", "foo", null, null, null)] + [InlineData("\"f\"\"oo\"", "f\"oo", null, null, null, null)] + [InlineData("character varying", "character varying", null, null, null, null)] + [InlineData("with_underscore", "with_underscore", null, null, null, null)] + [InlineData("varchar(30)", "varchar", null, 30, null, null)] + [InlineData("varchar(30)[]", "varchar(30)[]", null, null, null, null)] + [InlineData("numeric(30)", "numeric", null, null, 30, null)] + [InlineData("numeric(30,3)", "numeric", null, null, 30, 3)] + public void ParseStoreType(string storeTypeName, string expectedName, string? expectedSchema, int? expectedSize, int? expectedPrecision, int? expectedScale) + { + NpgsqlTypeMappingSource.ParseStoreTypeName( + storeTypeName, out var name, out var schema, out var size, out var precision, out var scale); + + Assert.Equal(expectedName, name); + Assert.Equal(expectedSchema, schema); + Assert.Equal(expectedSize, size); + Assert.Equal(expectedPrecision, precision); + Assert.Equal(expectedScale, scale); + } +#nullable restore + #region Support private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion = null) @@ -308,13 +338,12 @@ private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty()), + []), new RelationalTypeMappingSourceDependencies( - new IRelationalTypeMappingSourcePlugin[] - { - new NpgsqlNetTopologySuiteTypeMappingSourcePlugin(new NpgsqlNetTopologySuiteOptions()), + [ + new NpgsqlNetTopologySuiteTypeMappingSourcePlugin(new NpgsqlNetTopologySuiteSingletonOptions()), new DummyTypeMappingSourcePlugin() - }), + ]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), options); } @@ -353,13 +382,9 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p } } - private class DummyType - { - } + private class DummyType; - private class UnknownType - { - } + private class UnknownType; protected IModel CreateEmptyModel() => CreateModelBuilder().Model.FinalizeModel(); diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index f2e558f1e..ea8f3663a 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -9,7 +9,6 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using Npgsql.NameTranslation; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; @@ -259,7 +258,7 @@ public void GenerateSqlLiteral_returns_macaddr_literal() [Fact] public void GenerateCodeLiteral_returns_macaddr_literal() => Assert.Equal( - @"System.Net.NetworkInformation.PhysicalAddress.Parse(""001122334455"")", + """System.Net.NetworkInformation.PhysicalAddress.Parse("001122334455")""", CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55"))); [Fact] @@ -270,7 +269,7 @@ public void GenerateSqlLiteral_returns_macaddr8_literal() [Fact] public void GenerateCodeLiteral_returns_macaddr8_literal() => Assert.Equal( - @"System.Net.NetworkInformation.PhysicalAddress.Parse(""0011223344556677"")", + """System.Net.NetworkInformation.PhysicalAddress.Parse("0011223344556677")""", CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55-66-77"))); [Fact] @@ -279,7 +278,7 @@ public void GenerateSqlLiteral_returns_inet_literal() [Fact] public void GenerateCodeLiteral_returns_inet_literal() - => Assert.Equal(@"System.Net.IPAddress.Parse(""192.168.1.1"")", CodeLiteral(IPAddress.Parse("192.168.1.1"))); + => Assert.Equal("""System.Net.IPAddress.Parse("192.168.1.1")""", CodeLiteral(IPAddress.Parse("192.168.1.1"))); [Fact] public void GenerateSqlLiteral_returns_cidr_literal() @@ -288,7 +287,7 @@ public void GenerateSqlLiteral_returns_cidr_literal() [Fact] public void GenerateCodeLiteral_returns_cidr_literal() => Assert.Equal( - @"new NpgsqlTypes.NpgsqlCidr(System.Net.IPAddress.Parse(""192.168.1.0""), (byte)24)", + """new NpgsqlTypes.NpgsqlCidr(System.Net.IPAddress.Parse("192.168.1.0"), (byte)24)""", CodeLiteral(new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), 24))); #endregion Networking @@ -402,19 +401,19 @@ public void GenerateSqlLiteral_returns_bool_literal() [Fact] public void GenerateSqlLiteral_returns_varbit_literal() - => Assert.Equal("B'10'", GetMapping("varbit").GenerateSqlLiteral(new BitArray(new[] { true, false }))); + => Assert.Equal("B'10'", GetMapping("varbit").GenerateSqlLiteral(new BitArray([true, false]))); [Fact] public void GenerateCodeLiteral_returns_varbit_literal() - => Assert.Equal("new System.Collections.BitArray(new bool[] { true, false })", CodeLiteral(new BitArray(new[] { true, false }))); + => Assert.Equal("new System.Collections.BitArray(new bool[] { true, false })", CodeLiteral(new BitArray([true, false]))); [Fact] public void GenerateSqlLiteral_returns_bit_literal() - => Assert.Equal("B'10'", GetMapping("bit").GenerateSqlLiteral(new BitArray(new[] { true, false }))); + => Assert.Equal("B'10'", GetMapping("bit").GenerateSqlLiteral(new BitArray([true, false]))); [Fact] public void GenerateCodeLiteral_returns_bit_literal() - => Assert.Equal("new System.Collections.BitArray(new bool[] { true, false })", CodeLiteral(new BitArray(new[] { true, false }))); + => Assert.Equal("new System.Collections.BitArray(new bool[] { true, false })", CodeLiteral(new BitArray([true, false]))); [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] public void ValueComparer_hstore_array() @@ -438,7 +437,7 @@ public void GenerateSqlLiteral_returns_bytea_literal() [Fact] public void GenerateSqlLiteral_returns_hstore_literal() => Assert.Equal( - @"HSTORE '""k1""=>""v1"",""k2""=>""v2""'", + """HSTORE '"k1"=>"v1","k2"=>"v2"'""", GetMapping("hstore").GenerateSqlLiteral(new Dictionary { { "k1", "v1" }, { "k2", "v2" } })); [Fact] @@ -454,7 +453,7 @@ public void GenerateSqlLiteral_returns_BigInteger_literal() [Fact] public void GenerateCodeLiteral_returns_BigInteger_literal() => Assert.Equal( - @"BigInteger.Parse(""18446744073709551615"", NumberFormatInfo.InvariantInfo)", + """BigInteger.Parse("18446744073709551615", NumberFormatInfo.InvariantInfo)""", CodeLiteral(new BigInteger(ulong.MaxValue))); [Fact] @@ -489,7 +488,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,9 +500,17 @@ 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)); + Assert.Equal(""" + 'sad'::"DummyEnum" + """, mapping.GenerateSqlLiteral(DummyEnum.Sad)); } private enum DummyEnum @@ -705,7 +716,7 @@ public void GenerateSqlLiteral_returns_tsrange_literal() Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); var value = new NpgsqlRange(new DateTime(2020, 1, 1, 12, 0, 0), new DateTime(2020, 1, 2, 12, 0, 0)); - Assert.Equal(@"'[""2020-01-01T12:00:00"",""2020-01-02T12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00","2020-01-02T12:00:00"]'::tsrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -716,7 +727,7 @@ public void GenerateSqlLiteral_returns_tstzrange_literal() var value = new NpgsqlRange( new DateTime(2020, 1, 1, 12, 0, 0, DateTimeKind.Utc), new DateTime(2020, 1, 2, 12, 0, 0, DateTimeKind.Utc)); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal("""'["2020-01-01T12:00:00Z","2020-01-02T12:00:00Z"]'::tstzrange""", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -900,12 +911,12 @@ public void GenerateSqlLiteral_returns_json_element_literal() public void GenerateCodeLiteral_returns_json_document_literal() => Assert.Equal( """System.Text.Json.JsonDocument.Parse("{\"Name\":\"Joe\",\"Age\":25}", new System.Text.Json.JsonDocumentOptions())""", - CodeLiteral(JsonDocument.Parse(@"{""Name"":""Joe"",""Age"":25}"))); + CodeLiteral(JsonDocument.Parse("""{"Name":"Joe","Age":25}"""))); [Fact] public void GenerateCodeLiteral_returns_json_element_literal() // TODO: https://github.com/dotnet/efcore/issues/32192 - => Assert.Throws(() => CodeLiteral(JsonDocument.Parse(@"{""Name"":""Joe"",""Age"":25}").RootElement)); + => Assert.Throws(() => CodeLiteral(JsonDocument.Parse("""{"Name":"Joe","Age":25}""").RootElement)); // Assert.Equal( // """System.Text.Json.JsonDocument.Parse("{\"Name\":\"Joe\",\"Age\":25}", new System.Text.Json.JsonDocumentOptions()).RootElement""", @@ -939,8 +950,8 @@ public void ValueComparer_JsonElement() Name = "Joe", Age = 25, IsVip = false, - Orders = new[] - { + Orders = + [ new Order { Price = 99.5m, @@ -953,7 +964,7 @@ public void ValueComparer_JsonElement() ShippingAddress = "Some address 2", ShippingDate = new DateTime(2019, 10, 10) } - } + ] }; public class Customer @@ -979,9 +990,9 @@ public class Order new TypeMappingSourceDependencies( new ValueConverterSelector(new ValueConverterSelectorDependencies()), new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - Array.Empty() + [] ), - new RelationalTypeMappingSourceDependencies(Array.Empty()), + new RelationalTypeMappingSourceDependencies([]), new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), new NpgsqlSingletonOptions() ); diff --git a/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs b/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs index edf36f126..61b17e88e 100644 --- a/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs +++ b/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs @@ -32,13 +32,8 @@ public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCo return serviceCollection; } - private sealed class ExtensionInfo : RelationalExtensionInfo + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : RelationalExtensionInfo(extension) { - public ExtensionInfo(IDbContextOptionsExtension extension) - : base(extension) - { - } - public override void PopulateDebugInfo(IDictionary debugInfo) { } diff --git a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs index fd1b15b1a..86fadf80f 100644 --- a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs +++ b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs @@ -82,9 +82,7 @@ public void MaxBatchSize_is_optional() Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); } - private class FakeDbContext : DbContext - { - } + private class FakeDbContext : DbContext; private static INonTrackedModificationCommand CreateModificationCommand( string name, diff --git a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs index 1fcd77b26..c242266e0 100644 --- a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs +++ b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs @@ -46,9 +46,7 @@ public void AddCommand_returns_false_when_max_batch_size_is_reached() CreateModificationCommand("T1", null, false))); } - private class FakeDbContext : DbContext - { - } + private class FakeDbContext : DbContext; private static INonTrackedModificationCommand CreateModificationCommand( string name,